{"repo": "chainlesschain/chainlesschain", "files": {"docs-site/docs/chainlesschain/ai-models.md": {"4c95005a30fd890e2e4175c2cceaa3f8d9e2fc05": {"t": 1785674644.7323923, "n": 38210, "h": "e175591b48b0bbef", "c": "# AI模型配置\n\n> **版本: v0.37.6 | 14+云LLM提供商 | 多模态支持 | 智能模型路由 | Context Engineering**\n\nChainlessChain 支持多种AI模型,包括本地部署和云端API,满足不同的使用需求。桌面端默认使用火山引擎豆包(VolcEngine Doubao),支持 Ollama 本地模型自动回退,后端 AI Service 支持 14+ 云LLM提供商。\n\n## 概述\n\nAI 模型配置模块管理 ChainlessChain 的多模型接入与智能路由。系统集成 14+ LLM 提供商(Ollama、OpenAI、Anthropic、DeepSeek、火山引擎等),通过 Multi-Model Router 按任务复杂度自动选择最优模型,支持多模态视觉理解、KV-Cache Context Engineering 优化和云端失败自动回退本地模型。\n\n## 核心特性\n\n- 🤖 **14+ 提供商**: Ollama、OpenAI、Anthropic、DeepSeek、火山引擎等全面集成\n- 🎯 **智能路由**: Multi-Model Router 按任务复杂度自动选择最优模型\n- 👁️ **多模态支持**: LLaVA 本地 + 豆包 Vision + GPT-4V 视觉理解\n- ⚡ **Context Engineering**: KV-Cache 优化,60-85% 缓存命中率\n- 🔄 **智能回退**: 云端失败自动回退 Ollama 本地模型\n- 📊 **性能监控**: Token 追踪、成本分析、LLM Performance Dashboard\n\n## 系统架构\n\n```\n┌─────────────────────────────────────────────────────┐\n│ AI 模型管理层 │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌────────────┐ ┌────────────┐ ┌───────────────┐ │\n│ │ Multi-Model│ │ Context │ │ Token │ │\n│ │ Router │ │ Engineering│ │ Tracker │ │\n│ │ (智能路由) │ │ (KV-Cache) │ │ (用量监控) │ │\n│ └─────┬──────┘ └──────┬─────┘ └───────────────┘ │\n│ │ │ │\n│ ┌─────▼────────────────▼──────────────────────┐ │\n│ │ LLM Provider Abstraction │ │\n│ ├─────┬──────┬──────┬───────┬──────┬──────────┤ │\n│ │Ollama│OpenAI│Claude│DeepSeek│豆包 │Custom │ │\n│ └─────┴──────┴──────┴───────┴──────┴──────────┘ │\n│ │\n│ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │\n│ │ Embedding│ │ RAG │ │ Vision Model │ │\n│ │ Cache │ │ Hybrid │ │ (LLaVA/GPT-4V) │ │\n│ └──────────┘ └──────────┘ └─────────────────┘ │\n└─────────────────────────────────────────────────────┘\n```\n\n## 模型类型\n\n### 1. LLM (大语言模型)\n\n用于对话、推理和内容生成:\n\n- **本地模型**: 隐私保护,无需联网\n- **云端API**: 性能最强,需要费用\n\n### 2. 多模态视觉模型\n\n用于图像理解、视频理解、GUI自动化:\n\n- **本地模型**: LLaVA (Ollama) - 离线视觉理解\n- **云端API**: 豆包Vision、GPT-4V/4o、Claude Vision\n\n### 3. Embedding模型\n\n用于文本向量化,支持语义搜索:\n\n- **本地模型**: 离线可用\n- **云端API**: 更高精度\n\n### 4. 图像/视频生成模型\n\n用于AI创作:\n\n- **图像生成**: 豆包Seedream、DALL-E\n- **视频生成**: 豆包Seedance、PixelDance\n\n### 5. RAG (检索增强生成)\n\n结合向量检索和LLM,提供知识库问答。混合搜索引擎支持 Vector + BM25 + RRF融合。\n\n## 支持的提供商总览\n\n| 提供商 | 类型 | 桌面端 | 后端AI Service | Function Calling |\n| -------------------------- | ---- | ----------- | -------------- | ---------------- |\n| **Ollama** | 本地 | ✅ | ✅ | - |\n| **火山引擎/豆包** | 云端 | ✅ (默认) | ✅ | ✅ |\n| **OpenAI** | 云端 | ✅ | ✅ | ✅ |\n| **Anthropic Claude** | 云端 | ✅ | - | - |\n| **DeepSeek** | 云端 | ✅ | ✅ | ✅ |\n| **通义千问 (DashScope)** | 云端 | - | ✅ | ✅ |\n| **智谱AI (GLM)** | 云端 | - | ✅ | ✅ |\n| **百度千帆** | 云端 | - | ✅ | - |\n| **腾讯混元** | 云端 | - | ✅ | - |\n| **讯飞星火** | 云端 | - | ✅ | - |\n| **MiniMax** | 云端 | - | ✅ | - |\n| **Moonshot/Kimi** | 云端 | ✅ (Custom) | - | - |\n| **硅基流动 (SiliconFlow)** | 云端 | ✅ (Custom) | - | - |\n| **零一万物 (Yi)** | 云端 | ✅ (Custom) | - | - |\n| **LLaVA** | 本地 | ✅ | - | - |\n| **自定义 (Custom)** | 云端 | ✅ | - | - |\n\n## 本地LLM部署\n\n### PC端部署\n\n#### 使用Ollama(推荐)\n\n```bash\n# 1. 安装Ollama\ncurl -fsSL https://ollama.ai/install.sh | sh\n\n# 或Windows\nwinget install Ollama.Ollama\n\n# 2. 启动Ollama服务\nollama serve\n\n# 3. 下载模型\nollama pull qwen2:7b # 通义千问7B\nollama pull llama3:8b # LLaMA3 8B\nollama pull phi3:mini # Phi-3 Mini 3.8B\nollama pull deepseek-coder:6.7b # DeepSeek Coder\nollama pull llava:7b # LLaVA视觉模型\n```\n\n#### 模型推荐\n\n| 模型 | 参数量 | 内存需求 | 速度 | 质量 | 适用场景 |\n| -------------- | ------ | --------- | ---------- | ---------- | -------- |\n| Phi-3 Mini | 3.8B | 8GB | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 低配PC |\n| Qwen2 | 7B | 16GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 通用推荐 |\n| LLaMA3 | 8B | 16GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 通用推荐 |\n| DeepSeek Coder | 6.7B | 16GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 代码生成 |\n| Qwen2 | 14B | 32GB | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 高性能PC |\n| LLaMA3 | 70B | 64GB+ GPU | ⭐⭐ | ⭐⭐⭐⭐⭐ | 专业用户 |\n| LLaVA | 7B | 16GB | ⭐⭐⭐ | ⭐⭐⭐⭐ | 视觉理解 |\n\n#### 配置Ollama\n\n在ChainlessChain中配置:\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"ollama\",\n \"llmConfig\": {\n \"baseURL\": \"http://localhost:11434\",\n \"model\": \"qwen2:7b\",\n \"temperature\": 0.7,\n \"maxTokens\": 2000,\n \"contextWindow\": 8192\n }\n }\n}\n```\n\n#### Docker部署(可选)\n\n```yaml\n# docker-compose.yml\nversion: \"3.8\"\n\nservices:\n ollama:\n image: ollama/ollama:latest\n container_name: chainlesschain-llm\n ports:\n - \"11434:11434\"\n volumes:\n - ./ollama_data:/root/.ollama\n environment:\n - OLLAMA_HOST=0.0.0.0\n deploy:\n resources:\n reservations:\n devices:\n - driver: nvidia\n count: all\n capabilities: [gpu]\n```\n\n启动:\n\n```bash\ndocker-compose up -d\ndocker exec -it chainlesschain-llm ollama pull qwen2:7b\n```\n\n### 移动端部署\n\n#### Android\n\n使用MLC LLM运行量化模型:\n\n```kotlin\n// 1. 添加依赖\ndependencies {\n implementation 'ai.mlc:mlc-llm-android:0.1.0'\n}\n\n// 2. 初始化模型\nclass LocalLLM(context: Context) {\n private val mlcEngine = MLCEngine(context)\n\n suspend fun initialize() {\n // 下载量化模型(约1-2GB)\n val modelPath = downloadModel(\"MiniCPM-2B-Q4\")\n mlcEngine.loadModel(modelPath)\n }\n\n suspend fun chat(prompt: String): String {\n return mlcEngine.generate(prompt, maxTokens = 512)\n }\n}\n```\n\n推荐模型:\n\n- **MiniCPM-2B**: 2.4B参数,移动端最优\n- **Gemma-2B**: 2B参数,速度快\n- **Phi-3 Mini**: 3.8B参数,质量好\n\n#### iOS\n\n使用Core ML:\n\n```swift\nimport CoreML\n\nclass LocalLLM {\n private var model: MLModel?\n\n func initialize() {\n // 加载.mlpackage模型\n let modelURL = Bundle.main.url(\n forResource: \"MiniCPM-2B\",\n withExtension: \"mlpackage\"\n )!\n model = try? MLModel(contentsOf: modelURL)\n }\n\n func generate(prompt: String) async -> String {\n let input = MLDictionary([\"input_text\": prompt])\n let output = try? model?.prediction(from: input)\n return output?[\"generated_text\"] as? String ?? \"\"\n }\n}\n```\n\n## 云端API配置\n\n### 火山引擎/豆包(默认提供商)\n\nChainlessChain 桌面端默认使用火山引擎豆包系列模型。\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"volcengine\",\n \"llmConfig\": {\n \"apiKey\": \"your-api-key\",\n \"baseURL\": \"https://ark.cn-beijing.volces.com/api/v3\",\n \"model\": \"doubao-seed-1-6-251015\",\n \"embeddingModel\": \"doubao-embedding-text-240715\"\n }\n }\n}\n```\n\n#### 文本生成模型\n\n| 模型 | 上下文 | 费用 (输入/输出 ¥/百万tokens) | 特点 |\n| ------------------------ | ------ | ----------------------------- | ------------------------ |\n| doubao-seed-1.6 | 256K | ¥0.8 / ¥2 | 主力模型,深度思考,推荐 |\n| doubao-seed-1.6-thinking | 256K | ¥1 / ¥2.5 | 强制思考模式 |\n| doubao-seed-1.6-flash | 256K | ¥0.4 / ¥1 | 快速响应,低延迟 |\n| doubao-seed-1.6-lite | 256K | ¥0.3 / ¥0.8 | 最低成本 |\n| doubao-pro-32k | 32K | ¥0.8 / ¥2 | 专业版 |\n| doubao-seed-code | 128K | ¥0.6 / ¥1.8 | 代码生成专用 |\n\n#### 视觉模型\n\n| 模型 | 上下文 | 费用 | 特点 |\n| ---------------------- | ------ | -------------------- | ----------------------------- |\n| doubao-seed-1.6-vision | 256K | ¥2.6/¥7.8/百万tokens | 视觉深度思考,GUI Agent,推荐 |\n| doubao-1.5-vision-pro | 128K | ¥2/¥6/百万tokens | 专业视觉理解 |\n| doubao-1.5-vision-lite | 64K | ¥1/¥3/百万tokens | 轻量视觉 |\n| doubao-1.5-ui-tars | - | ¥2/¥6/百万tokens | GUI自动化专用 |\n\n#### 图像/视频生成模型\n\n| 模型 | 费用 | 特点 |\n| ----------------------- | ------------- | ------------------------------ |\n| doubao-seedream-4.5 | ¥0.08-0.15/张 | 最新图像生成,推荐 |\n| doubao-seedream-4.0 | ¥0.06-0.12/张 | 上一代图像生成 |\n| doubao-seededit-3.0 | ¥0.05/张 | 图像编辑(背景移除、光线调整) |\n| doubao-seedance-1.5-pro | ¥0.3/秒 | 专业视频生成,推荐 |\n| doubao-pixeldance | ¥0.3/秒 | 高动态视频生成 |\n\n#### Embedding模型\n\n| 模型 | 维度 | 费用 | 特点 |\n| ----------------------- | ---- | ---------------- | ---------------- |\n| doubao-embedding-large | 2048 | ¥0.2/百万tokens | 高精度检索,推荐 |\n| doubao-embedding | 1024 | ¥0.15/百万tokens | 标准,中英文 |\n| doubao-embedding-vision | 1536 | ¥0.3/百万tokens | 图像向量化 |\n\n#### 专用模型\n\n| 模型 | 费用 | 特点 |\n| ----------------------- | -------------------- | ---------- |\n| doubao-seed-translation | ¥0.5/¥1.5/百万tokens | 翻译专用 |\n| doubao-seed3d-1.0 | ¥1.5/个 | 3D模型生成 |\n\n---\n\n### DeepSeek\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"deepseek\",\n \"llmConfig\": {\n \"apiKey\": \"sk-...\",\n \"baseURL\": \"https://api.deepseek.com/v1\",\n \"model\": \"deepseek-chat\"\n }\n }\n}\n```\n\n#### 支持的模型\n\n| 模型 | 费用 (输入/输出) | 上下文 | 特点 |\n| ----------------- | ------------------------- | ------ | -------------------- |\n| deepseek-chat | $0.14 / $0.28 / 1M tokens | 128K | 通用对话,性价比极高 |\n| deepseek-coder | $0.14 / $0.28 / 1M tokens | 128K | 代码生成专用 |\n| deepseek-reasoner | $0.55 / $2.19 / 1M tokens | 128K | 深度推理(R1) |\n\n> DeepSeek 支持 Function Calling,桌面端和后端均已集成。\n\n---\n\n### OpenAI\n\n#### 注册和获取API Key\n\n```\n1. 访问 https://platform.openai.com\n2. 注册账号\n3. API Keys → Create new secret key\n4. 复制保存API Key (sk-...)\n```\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"openai\",\n \"llmConfig\": {\n \"apiKey\": \"sk-...\",\n \"model\": \"gpt-4o\",\n \"baseURL\": \"https://api.openai.com/v1\",\n \"temperature\": 0.7,\n \"maxTokens\": 2000\n }\n }\n}\n```\n\n#### 支持的模型\n\n| 模型 | 费用 (输入/输出) | 上下文 | 特点 |\n| ----------- | ------------------------- | ------ | ---------------- |\n| gpt-4o | $2.50 / $10 / 1M tokens | 128K | 最新旗舰,多模态 |\n| gpt-4o-mini | $0.15 / $0.60 / 1M tokens | 128K | 快速便宜 |\n| o1 | $15 / $60 / 1M tokens | 200K | 推理模型 |\n| o3-mini | $1.10 / $4.40 / 1M tokens | 200K | 推理模型(轻量) |\n| gpt-4-turbo | $10 / $30 / 1M tokens | 128K | 长上下文 |\n\n---\n\n### Anthropic Claude\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"anthropic\",\n \"llmConfig\": {\n \"apiKey\": \"sk-ant-...\",\n \"model\": \"claude-sonnet-4-5-20250929\",\n \"temperature\": 0.7,\n \"maxTokens\": 4096\n }\n }\n}\n```\n\n#### 模型对比\n\n| 模型 | 费用 | 上下文 | 特点 |\n| -------------------------- | --------------- | ------ | ----------------------------------------------------------------------- |\n| claude-opus-4-6 | $5 / $25 / 1M | 200K | 最强推理和编码(Opus 4.5+ 已降价;$15/$75 仅适用已退役的 Opus 4 / 4.1) |\n| claude-sonnet-4-5-20250929 | $3 / $15 / 1M | 200K | 平衡性能 |\n| claude-haiku-4-5-20251001 | $0.80 / $4 / 1M | 200K | 快速便宜 |\n\n---\n\n### 国内大模型\n\n#### 通义千问 (DashScope)\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"qwen\",\n \"llmConfig\": {\n \"apiKey\": \"sk-...\",\n \"model\": \"qwen-max\",\n \"baseURL\": \"https://dashscope.aliyuncs.com/api/v1\"\n }\n }\n}\n```\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=dashscope\nDASHSCOPE_API_KEY=sk-...\nDASHSCOPE_MODEL=qwen-turbo\n```\n\n#### 智谱AI (ChatGLM)\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"zhipu\",\n \"llmConfig\": {\n \"apiKey\": \"your-zhipu-key\",\n \"model\": \"glm-4\"\n }\n }\n}\n```\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=zhipu\nZHIPU_API_KEY=your-key\nZHIPU_MODEL=glm-4\n```\n\n> 智谱AI 支持 Function Calling。\n\n#### 百度千帆 (文心一言)\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"ernie\",\n \"llmConfig\": {\n \"apiKey\": \"your_key:secret\",\n \"model\": \"ERNIE-Bot-4\"\n }\n }\n}\n```\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=qianfan\nQIANFAN_API_KEY=ak:sk\nQIANFAN_MODEL=ERNIE-Bot-turbo\n```\n\n#### 腾讯混元\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=hunyuan\nHUNYUAN_API_KEY=your-key\nHUNYUAN_MODEL=hunyuan-lite\n```\n\n> 使用 OpenAI 兼容接口 (`https://api.hunyuan.cloud.tencent.com/v1`)。\n\n#### 讯飞星火\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=spark\nSPARK_API_KEY=app_id:api_key:api_secret\nSPARK_MODEL=spark-lite\n```\n\n> 使用 OpenAI 兼容接口 (`https://spark-api-open.xf-yun.com/v1`)。\n\n#### MiniMax\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=minimax\nMINIMAX_API_KEY=your-key\nMINIMAX_MODEL=abab5.5-chat\n```\n\n> 使用 OpenAI 兼容接口 (`https://api.minimax.chat/v1`)。\n\n#### Moonshot/Kimi\n\n通过桌面端 Custom 提供商或 OpenAI 兼容接口接入:\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"custom\",\n \"llmConfig\": {\n \"apiKey\": \"your-moonshot-key\",\n \"baseURL\": \"https://api.moonshot.cn/v1\",\n \"model\": \"moonshot-v1-8k\",\n \"name\": \"Moonshot\"\n }\n }\n}\n```\n\n支持模型:`moonshot-v1-8k`, `moonshot-v1-32k`, `moonshot-v1-128k`(长上下文)\n\n#### 硅基流动 (SiliconFlow)\n\n聚合多种开源模型,价格极低:\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"custom\",\n \"llmConfig\": {\n \"apiKey\": \"your-siliconflow-key\",\n \"baseURL\": \"https://api.siliconflow.cn/v1\",\n \"model\": \"Qwen/Qwen2.5-72B-Instruct\",\n \"name\": \"SiliconFlow\"\n }\n }\n}\n```\n\n支持模型:`Qwen2-7B`, `Qwen2.5-72B`, `DeepSeek-V2.5` 等,费用低至 ¥0.7/百万tokens\n\n#### 零一万物 (Yi)\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"custom\",\n \"llmConfig\": {\n \"apiKey\": \"your-yi-key\",\n \"baseURL\": \"https://api.lingyiwanwu.com/v1\",\n \"model\": \"yi-large\",\n \"name\": \"Yi\"\n }\n }\n}\n```\n\n---\n\n## 多模态视觉模型\n\n### LLaVA 本地视觉模型\n\nChainlessChain 内置 LLaVA 客户端,支持本地多模态视觉理解:\n\n```bash\n# 安装LLaVA模型\nollama pull llava:7b\n```\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"visionModel\": \"llava:7b\",\n \"visionConfig\": {\n \"baseURL\": \"http://localhost:11434\",\n \"timeout\": 300000,\n \"maxImageSize\": 5242880\n }\n }\n}\n```\n\n支持的图片格式:`.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.bmp`\n\n### 云端视觉模型\n\n| 提供商 | 模型 | 特点 |\n| --------- | ---------------------- | ----------------------- |\n| 火山引擎 | doubao-seed-1.6-vision | 视觉深度思考,GUI Agent |\n| OpenAI | gpt-4o | 图像+视频理解 |\n| Anthropic | claude-sonnet-4-5 | 图像理解 |\n\n> Computer Use 功能集成了 Vision AI,支持自动截图+视觉分析实现GUI自动化。详见 [Computer Use →](/chainlesschain/computer-use)\n\n---\n\n## 智能模型路由\n\n### Multi-Model Router 技能\n\nChainlessChain 内置智能模型路由技能 (`/multi-model-router`),根据任务复杂度自动选择最优模型:\n\n```\n/multi-model-router --route fix a typo in README.md\n→ Complexity: 1/10. Recommended: haiku (fastest, cheapest).\n\n/multi-model-router --route refactor the entire authentication system across 15 files\n→ Complexity: 9/10. Recommended: opus (strongest reasoning).\n```\n\n#### 模型能力矩阵\n\n| 模型 | 推理 | 编码 | 速度 | 成本 | 适用场景 |\n| --------------- | ---- | ---- | ---- | ---- | ------------------- |\n| opus | 10 | 9 | 3 | 10 | 架构设计、复杂调试 |\n| sonnet | 8 | 9 | 7 | 5 | 代码生成、重构 |\n| haiku | 5 | 6 | 10 | 1 | 简单编辑、格式化 |\n| gpt-4o | 9 | 8 | 6 | 7 | 通用任务 |\n| gpt-4o-mini | 6 | 6 | 9 | 2 | 快速问答、翻译 |\n| qwen2 | 7 | 7 | 8 | 0 | 本地推理(免费) |\n| doubao-seed-1.6 | 8 | 8 | 7 | 2 | 通用+深度思考 |\n| deepseek-chat | 8 | 9 | 7 | 1 | 代码+推理(极低成本) |\n\n### 火山引擎智能模型选择器\n\n桌面端内置 VolcEngine Model Selector,支持按任务类型自动选择豆包模型:\n\n```javascript\n// 任务类型 → 自动选择最优模型\nconst taskTypes = {\n chat: \"doubao-seed-1.6\", // 通用对话\n long_context: \"doubao-seed-1.6\", // 长文本处理\n complex_reasoning: \"doubao-seed-1.6-thinking\", // 复杂推理\n image_understanding: \"doubao-seed-1.6-vision\", // 图像理解\n code_writing: \"doubao-seed-code\", // 代码编写\n fast_response: \"doubao-seed-1.6-flash\", // 快速响应\n cost_effective: \"doubao-seed-1.6-lite\", // 成本优化\n gui_automation: \"doubao-1.5-ui-tars\", // GUI自动化\n translation: \"doubao-seed-translation\", // 翻译\n};\n```\n\n### 自动回退机制\n\n后端 AI Service 具备智能 Fallback 机制:\n\n- 当配置的云LLM API Key 未设置或为空时,自动回退到 Ollama 本地模型\n- 确保服务始终可用,不因API配置缺失而中断\n\n---\n\n## Context Engineering (KV-Cache优化)\n\nChainlessChain 集成 Manus 风格的 Context Engineering 优化:\n\n### 核心优化\n\n- **静态/动态分离**: 将静态系统提示词放在对话开头,提高 KV-Cache 命中率 (60-85%)\n- **工具定义序列化**: 按名称确定性排序,避免缓存失效\n- **任务目标重述**: 在上下文中间重述目标,防止\"迷失在中间\"\n- **错误历史追踪**: 从过去的错误中学习\n- **可恢复压缩**: 保留引用以便后续恢复\n- **Token估算**: 中英文自动检测\n\n### 配置\n\n```json\n{\n \"ai\": {\n \"contextEngineering\": {\n \"enabled\": true,\n \"kvCacheOptimization\": true,\n \"toolMasking\": true,\n \"taskTracking\": true,\n \"maxContextTokens\": 128000\n }\n }\n}\n```\n\n> 详见 Context Engineering IPC (17个处理器) 的完整配置。\n\n---\n\n## Embedding模型配置\n\n### 本地Embedding\n\n#### 使用sentence-transformers\n\n```python\nfrom sentence_transformers import SentenceTransformer\n\n# PC端 - 大模型\nmodel = SentenceTransformer('BAAI/bge-large-zh-v1.5')\n\n# 移动端 - 小模型\nmodel_mobile = SentenceTransformer('BAAI/bge-small-zh-v1.5')\n\n# 向量化\nembeddings = model.encode(['文本1', '文本2'])\n```\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"embeddingProvider\": \"local\",\n \"embeddingConfig\": {\n \"model\": \"BAAI/bge-large-zh-v1.5\",\n \"dimensions\": 1024,\n \"maxBatchSize\": 32,\n \"device\": \"cuda\"\n }\n }\n}\n```\n\n### 云端Embedding\n\n#### OpenAI Embeddings\n\n```json\n{\n \"ai\": {\n \"embeddingProvider\": \"openai\",\n \"embeddingConfig\": {\n \"apiKey\": \"sk-...\",\n \"model\": \"text-embedding-3-small\",\n \"dimensions\": 1536\n }\n }\n}\n```\n\n费用: $0.02 / 1M tokens (text-embedding-3-small)\n\n#### 火山引擎 Embedding\n\n```json\n{\n \"ai\": {\n \"embeddingProvider\": \"volcengine\",\n \"embeddingConfig\": {\n \"apiKey\": \"your-api-key\",\n \"model\": \"doubao-embedding-text-240715\",\n \"dimensions\": 1024\n }\n }\n}\n```\n\n费用: ¥0.15 / 百万tokens\n\n---\n\n## RAG配置\n\n### 基本配置\n\n```json\n{\n \"ai\": {\n \"ragEnabled\": true,\n \"ragConfig\": {\n \"topK\": 5,\n \"minScore\": 0.7,\n \"reranking\": true,\n \"contextWindow\": 4000,\n \"chunkSize\": 500,\n \"chunkOverlap\": 50\n }\n }\n}\n```\n\n### 高级配置\n\n#### 混合检索\n\nChainlessChain 内置 Hybrid Search Engine,支持 Vector + BM25 双路检索 + RRF融合:\n\n```json\n{\n \"ragConfig\": {\n \"retrievalMode\": \"hybrid\",\n \"vectorWeight\": 0.6,\n \"keywordWeight\": 0.4,\n \"fusion\": \"rrf\"\n }\n}\n```\n\n- **Vector Search**: 语义相似度,通过 RAG Manager Embedding\n- **BM25 Search**: Okapi BM25 算法,中英文分词器\n- **RRF Fusion**: Reciprocal Rank Fusion 结果合并\n- **性能**: <20ms 搜索延迟,并行执行\n\n#### 重排序\n\n```json\n{\n \"ragConfig\": {\n \"reranking\": true,\n \"rerankerModel\": \"BAAI/bge-reranker-large\",\n \"rerankerTopK\": 3\n }\n}\n```\n\n---\n\n## 查询模板\n\n### 创建模板\n\n```\n设置 → AI → 查询模板 → 新建\n\n模板信息:\n- 名称: 编程助手\n- 关联知识库: 技术文档、代码笔记\n- LLM模型: qwen2:7b\n- RAG模型: bge-large-zh-v1.5\n- 系统提示词: (见下方)\n- Temperature: 0.3\n- Max Tokens: 3000\n```\n\n### 系统提示词示例\n\n#### 通用问答\n\n```\n你是一个智能助手,基于用户的个人知识库回答问题。\n请遵循以下原则:\n1. 只使用检索到的知识库内容回答\n2. 如果知识库中没有相关信息,明确告知用户\n3. 提供详细准确的答案\n4. 必要时引用原文\n```\n\n#### 编程助手\n\n```\n你是一个专业的编程助手,帮助用户解决编程问题。\n知识库包含用户的技术笔记和代码片段。\n\n回答要求:\n1. 基于知识库中的代码示例\n2. 提供完整可运行的代码\n3. 解释代码逻辑\n4. 指出潜在问题\n5. 建议最佳实践\n```\n\n#### 文案优化\n\n```\n你是一个专业的文案编辑,帮助用户优化文字表达。\n\n任务:\n1. 分析用户的草稿\n2. 提出改进建议\n3. 提供优化后的版本\n4. 保持原意,提升表达质量\n```\n\n---\n\n## LLM性能监控\n\nChainlessChain 内置 LLM Performance Dashboard(路由: `#/llm/performance`),提供全面的模型使用分析:\n\n### 监控指标\n\n- **Token 使用量**: 输入/输出 Token 统计,每日趋势\n- **成本分析**: 各提供商费用追踪,成本预测\n- **响应延迟**: 首 Token 时间、完整响应时间分布\n- **缓存命中率**: Embedding 缓存、Response 缓存效果\n- **模型对比**: 不同模型的性能/成本对比\n\n### 配置\n\n```json\n{\n \"ai\": {\n \"tokenTracking\": {\n \"enabled\": true,\n \"budgetAlert\": {\n \"daily\": 100000,\n \"monthly\": 2000000\n }\n }\n }\n}\n```\n\n> 详见 [LLM Performance Dashboard 文档](https://github.com/chainlesschain/chainlesschain/blob/main/docs/features/LLM_PERFORMANCE_DASHBOARD.md)\n\n---\n\n## 性能优化\n\n### 缓存策略\n\n```json\n{\n \"ai\": {\n \"cacheEnabled\": true,\n \"cacheConfig\": {\n \"embeddingCache\": true,\n \"embeddingCacheTTL\": 86400,\n \"llmResponseCache\": false,\n \"maxCacheSize\": 1000\n }\n }\n}\n```\n\nEmbedding 缓存使用 SQLite 存储,避免重复计算向量。\n\n### 批处理\n\n```python\n# 批量向量化\ntexts = ['文本1', '文本2', ..., '文本100']\n\n# 不推荐:逐个处理\nembeddings = [model.encode(text) for text in texts]\n\n# 推荐:批处理\nembeddings = model.encode(texts, batch_size=32)\n```\n\n### GPU加速\n\n```json\n{\n \"ai\": {\n \"device\": \"cuda\",\n \"gpuMemoryFraction\": 0.8,\n \"precision\": \"fp16\"\n }\n}\n```\n\n---\n\n## 成本优化\n\n### 混合部署策略\n\n```typescript\nclass AIRouter {\n async route(query: string, context: any) {\n // 1. 简单问题 → 本地小模型\n if (context.length < 1000) {\n return this.localLLM.generate(query);\n }\n\n // 2. 复杂问题 → 本地大模型\n if (this.localLargeModel.available && !this.needHighQuality(query)) {\n return this.localLargeModel.generate(query);\n }\n\n // 3. 高质量要求 → 云端API\n return this.cloudAPI.generate(query);\n }\n}\n```\n\n### Token使用优化\n\n```typescript\n// 1. 精简提示词\nconst systemPrompt = \"你是助手,简洁回答。\"; // 而非长篇大论\n\n// 2. 限制上下文\nconst context = relevantDocs.slice(0, 3); // 只用最相关的3个文档\n\n// 3. 控制输出长度\nconst maxTokens = 500; // 根据需求调整\n\n// 4. 使用Context Engineering优化KV-Cache命中率\n// 静态内容前置,动态内容后置\n```\n\n### 成本对比\n\n| 方案 | 月成本估算 | 适用场景 |\n| ---------------------- | ---------- | --------------------- |\n| 纯本地 (Ollama) | ¥0 (电费) | 隐私优先、离线场景 |\n| 豆包 Seed 1.6 Lite | ¥5-50 | 日常对话、性价比首选 |\n| DeepSeek Chat | ¥5-30 | 代码+推理、极致性价比 |\n| GPT-4o-mini | ¥10-100 | 通用任务 |\n| GPT-4o / Claude Sonnet | ¥50-500 | 专业开发 |\n| GPT-o1 / Claude Opus | ¥200-2000 | 复杂推理 |\n\n---\n\n## 模型评估\n\n### 质量评估\n\n```python\n# 评估检索质量\ndef evaluate_retrieval(test_queries, ground_truth):\n scores = {\n 'precision': [],\n 'recall': [],\n 'ndcg': []\n }\n\n for query, truth in zip(test_queries, ground_truth):\n retrieved = rag.retrieve(query)\n scores['precision'].append(precision(retrieved, truth))\n scores['recall'].append(recall(retrieved, truth))\n scores['ndcg'].append(ndcg(retrieved, truth))\n\n return scores\n\n# 评估生成质量\ndef evaluate_generation(test_cases):\n scores = []\n for query, expected_answer in test_cases:\n generated = llm.generate(query)\n score = similarity(generated, expected_answer)\n scores.append(score)\n return np.mean(scores)\n```\n\n### 性能监控\n\n```\n设置 → AI → 性能监控\n\n指标:\n- 平均响应时间\n- 每日Token消耗\n- 检索精度\n- 用户满意度(评分)\n- KV-Cache命中率\n\n图表:\n- 每日使用量趋势\n- 不同模型的成本对比\n- 响应速度分布\n- 缓存命中率趋势\n```\n\n---\n\n## 使用示例\n\n### Ollama 本地模型配置与使用\n\n```bash\n# 安装并启动 Ollama\nollama serve\n\n# 下载推荐模型\nollama pull qwen2:7b\n\n# 通过 CLI 测试模型连通性\nchainlesschain llm test\nchainlesschain llm models # 列出已安装模型\n```\n\n### 切换 LLM 提供商\n\n```bash\n# 切换到 DeepSeek(高性价比云端)\nchainlesschain llm switch deepseek\nchainlesschain config set llm.apiKey sk-xxx\n\n# 切换到 Ollama 本地模型\nchainlesschain llm switch ollama\n\n# 查看当前提供商状态\nchainlesschain llm providers\n```\n\n### 在对话中使用不同模型\n\n```bash\n# 单次问答(使用当前配置的模型)\nchainlesschain ask \"解释量子计算的基本原理\"\n\n# 交互式对话(支持流式输出)\nchainlesschain chat\n\n# Agent 模式(146 技能 + Plan Mode)\nchainlesschain agent\n```\n\n---\n\n## 故障排查\n\n### 本地模型加载失败\n\n**常见问题**:\n\n```\nError: Failed to load model\n\n原因:\n1. 内存不足\n2. 模型文件损坏\n3. 依赖缺失\n\n解决:\n1. 检查内存: htop / 任务管理器\n2. 重新下载模型: ollama pull qwen2:7b\n3. 检查日志: ~/.ollama/logs/\n```\n\n### API调用失败\n\n**检查清单**:\n\n```\n□ API Key是否正确\n□ 账户余额是否充足\n□ 网络连接是否正常\n□ 是否超过速率限制\n□ baseURL是否正确\n□ 提供商服务是否正常\n```\n\n### 智能Fallback失效\n\n如果云端API失败但没有自动回退到Ollama:\n\n```\n检查:\n1. Ollama服务是否运行: curl http://localhost:11434/api/tags\n2. 本地模型是否已下载: ollama list\n3. 配置中fallback是否启用\n```\n\n### 响应速度慢\n\n**优化步骤**:\n\n```\n1. 使用更快的模型(doubao-seed-1.6-flash、Phi-3、Gemma)\n2. 减少上下文长度\n3. 启用GPU加速\n4. 使用量化模型(Q4, Q8)\n5. 启用Context Engineering的KV-Cache优化\n6. 考虑使用云端API\n```\n\n---\n\n## 常见问题\n\n### 本地模型vs云端API,如何选择?\n\n```\n本地模型适合:\n✓ 隐私敏感数据\n✓ 离线使用场景\n✓ 长期大量使用(成本低)\n✓ 实时响应要求\n\n云端API适合:\n✓ 对质量要求极高\n✓ 使用量不大\n✓ 无本地算力\n✓ 需要最新功能\n```\n\n### 如何降低成本?\n\n```\n策略:\n1. 本地模型为主,云端为辅\n2. 使用Multi-Model Router按任务复杂度路由\n3. 缓存常见问题的答案(Embedding Cache + Response Cache)\n4. 优化提示词,减少Token消耗\n5. 使用Context Engineering提升KV-Cache命中率\n6. 使用更便宜的模型(DeepSeek、豆包Lite、GPT-4o-mini、Haiku)\n7. 批处理请求\n```\n\n### 模型推荐?\n\n```\n个人使用:\n- PC: Qwen2:7B (本地) + 豆包Seed 1.6 Lite (云端,最低成本)\n- 移动: MiniCPM-2B (本地)\n- 云端首选: DeepSeek Chat (性价比之王)\n\n专业开发者:\n- PC: LLaMA3:70B (本地) + Claude Sonnet 4.5 (云端)\n- 代码: DeepSeek Coder + doubao-seed-code\n- 推理: DeepSeek Reasoner / Claude Opus 4.6\n\n企业:\n- 自建: LLaMA3:70B 集群\n- 国内云端: 豆包Seed 1.6 (默认) / 通义千问Max\n- 国际云端: GPT-4o / Claude Sonnet 4.5\n- 合规审计: 配合Enterprise Audit系统\n```\n\n### 为什么默认使用火山引擎豆包?\n\n```\n原因:\n1. 国内访问稳定,无需代理\n2. 256K超长上下文窗口\n3. 价格极具竞争力(¥0.3-2/百万tokens)\n4. 支持深度思考模式\n5. 完整多模态能力(视觉/图像/视频/3D)\n6. 原生Function Calling支持\n7. 中文能力优秀\n```\n\n---\n\n## 已实现功能\n\n- [x] 14+ 云LLM提供商集成(Ollama、OpenAI、Anthropic、DeepSeek、火山引擎、通义千问、智谱AI、百度千帆、腾讯混元、讯飞星火、MiniMax等)\n- [x] 多模态视觉模型(LLaVA本地 + 豆包Vision + GPT-4V + Claude Vision)\n- [x] 智能模型路由(Multi-Model Router技能)\n- [x] Context Engineering KV-Cache优化\n- [x] LLM Performance Dashboard(Token追踪、成本分析)\n- [x] 混合检索引擎(Vector + BM25 + RRF融合)\n- [x] Embedding缓存(SQLite存储)\n- [x] 响应缓存和流式输出\n- [x] 火山引擎智能模型选择器(按任务类型自动选择)\n- [x] 图像生成(豆包Seedream 3.0/4.0/4.5)\n- [x] 视频生成(豆包Seedance、PixelDance)\n- [x] 图像编辑(豆包SeedEdit)\n- [x] 3D模型生成(doubao-seed3d-1.0)\n- [x] GUI自动化模型(doubao-1.5-ui-tars)\n- [x] 翻译专用模型(doubao-seed-translation)\n- [x] 智能Fallback(云端失败自动回退Ollama)\n- [x] API Key加密存储(AES-256-GCM)\n\n## 未来规划\n\n- [x] 模型微调支持(LoRA/QLoRA本地微调)\n- [x] 语音模型集成(Whisper语音识别 + TTS合成)\n- [x] 联邦学习(分布式模型训练,保护隐私)\n- [x] 更多模型支持(Google Gemini、Mistral、零一万物Yi等)\n- [x] 本地模型量化工具(一键GGUF/GPTQ量化)\n- [x] 模型性能基准测试(自动化评估框架)\n- [x] 多模型协作(Architect+Editor双模型模式)\n- [x] 长期记忆增强(结合Permanent Memory的个性化模型)\n\n## 配置参考\n\n### 完整 LLM 提供商配置结构\n\n```js\n// desktop-app-vue/src/main/config/unified-config-manager.js\n// .chainlesschain/config.json — ai 段完整字段\n{\n \"ai\": {\n // 主提供商选择: \"ollama\" | \"volcengine\" | \"openai\" | \"anthropic\" |\n // \"deepseek\" | \"qwen\" | \"zhipu\" | \"ernie\" | \"custom\"\n \"llmProvider\": \"volcengine\",\n\n // 提供商连接参数 (与 llmProvider 对应)\n \"llmConfig\": {\n \"apiKey\": \"your-api-key\", // 云端提供商 API Key(AES-256-GCM 加密存储)\n \"baseURL\": \"https://ark.cn-beijing.volces.com/api/v3\", // 自定义端点\n \"model\": \"doubao-seed-1.6-251015\", // 文本生成主模型\n \"embeddingModel\": \"doubao-embedding-text-240715\", // Embedding 模型\n \"temperature\": 0.7, // 采样温度 (0.0–1.0)\n \"maxTokens\": 4096, // 单次最大输出 token\n \"contextWindow\": 131072 // 上下文窗口大小\n },\n\n // 视觉模型单独配置\n \"visionModel\": \"doubao-seed-1.6-vision\",\n \"visionConfig\": {\n \"baseURL\": \"http://localhost:11434\", // 本地 LLaVA 时填 Ollama 地址\n \"timeout\": 300000,\n \"maxImageSize\": 5242880 // 最大图片尺寸 (bytes)\n },\n\n // Embedding 提供商: \"local\" | \"openai\" | \"volcengine\"\n \"embeddingProvider\": \"volcengine\",\n \"embeddingConfig\": {\n \"apiKey\": \"your-api-key\",\n \"model\": \"doubao-embedding-text-240715\",\n \"dimensions\": 1024,\n \"maxBatchSize\": 32\n },\n\n // Context Engineering (KV-Cache 优化)\n \"contextEngineering\": {\n \"enabled\": true,\n \"kvCacheOptimization\": true, // 静态/动态内容分离\n \"toolMasking\": true, // 按激活技能过滤工具集\n \"taskTracking\": true, // 任务目标重述\n \"maxContextTokens\": 128000\n },\n\n // RAG 混合检索\n \"ragEnabled\": true,\n \"ragConfig\": {\n \"retrievalMode\": \"hybrid\", // \"vector\" | \"bm25\" | \"hybrid\"\n \"topK\": 5,\n \"minScore\": 0.7,\n \"vectorWeight\": 0.6,\n \"keywordWeight\": 0.4,\n \"fusion\": \"rrf\", // Reciprocal Rank Fusion\n \"reranking\": true,\n \"rerankerModel\": \"BAAI/bge-reranker-large\",\n \"contextWindow\": 4000,\n \"chunkSize\": 500,\n \"chunkOverlap\": 50\n },\n\n // Token 用量追踪与预算\n \"tokenTracking\": {\n \"enabled\": true,\n \"budgetAlert\": {\n \"daily\": 100000, // 日预算 token 上限\n \"monthly\": 2000000 // 月预算 token 上限\n }\n },\n\n // Embedding 响应缓存\n \"cacheEnabled\": true,\n \"cacheConfig\": {\n \"embeddingCache\": true,\n \"embeddingCacheTTL\": 86400, // 秒,默认 24h\n \"llmResponseCache\": false,\n \"maxCacheSize\": 1000\n },\n\n // GPU 加速 (Embedding / 本地推理)\n \"device\": \"cuda\", // \"cpu\" | \"cuda\" | \"mps\"\n \"gpuMemoryFraction\": 0.8,\n \"precision\": \"fp16\"\n }\n}\n```\n\n### 模型智能路由配置\n\n```js\n// llm-manager.js — LLM_CATEGORIES 与 Category Routing\n// 用于 resolveCategory(category, { skill }) 方法\n\n// 可选类别值\nconst LLM_CATEGORIES = {\n QUICK: \"quick\", // 补全/简单改写 → 优先本地 ollama\n DEEP: \"deep\", // 长上下文/架构 → 优先 anthropic / openai\n REASONING: \"reasoning\", // 推理密集 → 优先 deepseek / o1\n VISION: \"vision\", // 多模态 → 优先 gemini / gpt-4o\n CREATIVE: \"creative\", // 文案/UI创作 → 优先 anthropic\n EMBEDDING: \"embedding\", // 文本向量化 → 优先 volcengine / openai\n AUDIO: \"audio\", // 语音识别/TTS → 优先 openai whisper\n ASR: \"asr\", // 语音转文字 → 优先 openai / gemini\n AUDIO_ANALYSIS: \"audio-analysis\", // 节拍检测/音频特征 → 优先本地 ollama\n VIDEO_VLM: \"video-vlm\", // 视频理解 VLM → 优先 gemini / openai\n};\n\n// SKILL.md 中声明 model-hints 触发自动类别推断\n// capability: \"reasoning\" → REASONING\n// capability: \"transcription\" → ASR\n// capability: \"beat-detection\" → AUDIO_ANALYSIS\n// capability: \"video-review\" → VIDEO_VLM\n// context-window: \"large\" → DEEP\n```\n\n### 自定义 OpenAI 兼容端点\n\n```js\n// 适用于 Moonshot、SiliconFlow、零一万物、本地 vLLM 等 OpenAI 兼容服务\n{\n \"ai\": {\n \"llmProvider\": \"custom\",\n \"llmConfig\": {\n \"apiKey\": \"your-provider-key\",\n \"baseURL\": \"https://api.siliconflow.cn/v1\", // 替换为实际端点\n \"model\": \"Qwen/Qwen2.5-72B-Instruct\",\n \"name\": \"SiliconFlow\", // 显示名称\n \"temperature\": 0.7,\n \"maxTokens\": 4096\n }\n }\n}\n```\n\n---\n\n## 性能指标\n\n### 推理延迟(首 Token 时间,TTFT)\n\n| 模型 | 硬件条件 | 平均 TTFT | P95 TTFT | 备注 |\n| ------------------------ | ------------- | --------- | -------- | ---------------- |\n| Phi-3 Mini 3.8B (Q4) | CPU 16GB RAM | 320 ms | 680 ms | 低配 PC 推荐 |\n| Qwen2 7B (Q4) | CPU 16GB RAM | 580 ms | 1200 ms | 通用本地推荐 |\n| Qwen2 7B (Q4) | GPU RTX 3060 | 95 ms | 180 ms | GPU 加速 |\n| LLaMA3 8B (Q4) | CPU 16GB RAM | 620 ms | 1300 ms | - |\n| LLaMA3 8B (Q4) | GPU RTX 3060 | 110 ms | 210 ms | GPU 加速 |\n| LLaMA3 70B (Q4) | GPU A100 40GB | 280 ms | 520 ms | 专业服务器 |\n| doubao-seed-1.6-flash | 云端 API | 180 ms | 420 ms | 低延迟模型 |\n| doubao-seed-1.6 | 云端 API | 350 ms | 780 ms | 标准主力模型 |\n| doubao-seed-1.6-thinking | 云端 API | 1200 ms | 3500 ms | 深度思考模式 |\n| deepseek-chat | 云端 API | 280 ms | 650 ms | 高性价比 |\n| gpt-4o | 云端 API | 420 ms | 950 ms | OpenAI 旗舰 |\n| gpt-4o-mini | 云端 API | 190 ms | 420 ms | 快速便宜 |\n| claude-sonnet-4-5 | 云端 API | 380 ms | 820 ms | Anthropic 平衡版 |\n\n### Token 吞吐量(输出速率)\n\n| 模型 | 硬件条件 | 平均吞吐 (tokens/s) | 备注 |\n| --------------------- | ------------- | ------------------- | ------------ |\n| Phi-3 Mini 3.8B (Q4) | CPU 16GB RAM | 12–18 t/s | 低配 PC |\n| Qwen2 7B (Q4) | CPU 16GB RAM | 8–14 t/s | 通用本地 |\n| Qwen2 7B (Q4) | GPU RTX 3060 | 45–65 t/s | GPU 加速 |\n| LLaMA3 8B (Q4) | GPU RTX 3060 | 50–70 t/s | GPU 加速 |\n| LLaMA3 70B (Q4) | GPU A100 40GB | 28–40 t/s | 专业服务器 |\n| doubao-seed-1.6-flash | 云端 API | 80–120 t/s | 低延迟模型 |\n| doubao-seed-1.6 | 云端 API | 45–80 t/s | 标准主力模型 |\n| deepseek-chat | 云端 API | 50–90 t/s | 高性价比 |\n| gpt-4o-mini | 云端 API | 70–110 t/s | 快速便宜 |\n| gpt-4o | 云端 API | 40–65 t/s | OpenAI 旗舰 |\n\n### Context Engineering KV-Cache 命中率\n\n| 场景 | 命中率 | Token 节省 | 说明 |\n| -------------------------- | ------ | ---------- | ------------------------- |\n| 单轮对话(短提示词) | 25–40% | 15–25% | 系统提示词命中 |\n| 多轮对话(同会话) | 60–75% | 40–55% | 历史上下文命中 |\n| Agent 模式(工具定义稳定) | 70–85% | 50–65% | 工具定义 + 系统提示词命中 |\n| RAG 增强问答 | 55–70% | 35–50% | 文档块重复检索命中 |\n\n### Embedding 与 RAG 性能\n\n| 指标 | 本地 (BGE-large) | 云端 (doubao-embedding) | 云端 (OpenAI ada-3) |\n| -------------------- | ---------------- | ----------------------- | ------------------- |\n| 向量化延迟 (单条) | 8–15 ms (GPU) | 35–60 ms | 40–70 ms |\n| 批处理吞吐 (32条) | 180–280 条/s | 受 API 限速 | 受 API 限速 |\n| 混合检索延迟 | <20 ms | <20 ms | <20 ms |\n| 检索精度 (MRR@5) | 0.81 | 0.87 | 0.85 |\n| Embedding 缓存命中率 | — | 65–80% | 65–80% |\n\n---\n\n## 测试覆盖率\n\n### LLM 核心模块测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| -------------------------------------------------------------------------------- | ------------------------------------------------------- | ------ |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/llm-manager.test.js` | LLMManager 多提供商路由、chat()、fallback | 48 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/llm-manager-category-routing.test.js` | Category Routing (quick/deep/reasoning/vision/creative) | 26 |\n| ✅ `desktop-app-vue/tests/unit/llm/llm-manager-media-categories.test.js` | 媒体类别扩展 (asr/audio-analysis/video-vlm) | 25 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/token-tracker.test.js` | Token 用量统计、成本计算、预算告警 | 22 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/prompt-compressor.test.js` | 去重/截断/总结三层压缩策略 | 18 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/context-engineering.test.js` | KV-Cache 优化、工具过滤、任务追踪 | 31 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/volcengine-model-selector.test.js` | 火山引擎任务类型→模型自动选择 | 19 |\n\n### RAG 与 Embedding 测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| ---------------------------------------------------------------- | ----------------------------------- | ------ |\n| ✅ `desktop-app-vue/tests/unit/rag/hybrid-search-engine.test.js` | Vector + BM25 双路检索 + RRF 融合 | 34 |\n| ✅ `desktop-app-vue/tests/unit/rag/embedding-cache.test.js` | SQLite Embedding 缓存读写、TTL 过期 | 16 |\n| ✅ `desktop-app-vue/tests/unit/rag/rag-manager.test.js` | RAG 检索、重排序、上下文组装 | 27 |\n\n### 多模态与视觉模型测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| -------------------------------------------------------------------- | -------------------------------- | ------ |\n| ✅ `desktop-app-vue/tests/unit/llm/llava-client.test.js` | LLaVA 本地视觉模型调用、图片编码 | 14 |\n| ✅ `desktop-app-vue/tests/unit/ai-engine/multi-model-router.test.js` | 任务复杂度评分、模型推荐逻辑 | 21 |\n\n### 视频生成集成测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| ------------------------------------------------------------ | ------------------------------------------- | ------ |\n| ✅ `desktop-app-vue/tests/unit/llm/volcengine-video.test.js` | Seedance text-to-video 任务提交/轮询 (mock) | 18 |\n| ✅ `packages/cli/__tests__/video/video-generate.test.js` | CLI video 生成命令端到端 (mock API) | 12 |\n\n### CLI LLM 命令测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| ------------------------------------------------------ | --------------------------------------------- | ------ |\n| ✅ `packages/cli/__tests__/unit/llm-providers.test.js` | CLI 提供商切换、providers 列表 | 17 |\n| ✅ `packages/cli/__tests__/unit/llm-switch.test.js` | `chainlesschain llm switch` 持久化配置 | 11 |\n| ✅ `packages/cli/__tests__/unit/stream-router.test.js` | StreamRouter NDJSON 输出、provider/model 透传 | 24 |\n\n---\n\n## 安全考虑\n\n- **API Key 加密存储**: 所有 API Key 使用 AES-256-GCM 加密后存储在本地 `config.json` 中,不以明文保存\n- **本地推理隐私**: 使用 Ollama 本地模型时数据完全离线,不发送到任何外部服务器\n- **传输加密**: 云端 API 调用强制使用 HTTPS/TLS 加密传输\n- **密钥轮换**: 建议定期更换云端 API Key,旧 Key 及时在提供商控制台吊销\n- **预算控制**: 启用 Token 追踪和预算告警(`tokenTracking.budgetAlert`),防止意外超支\n- **敏感数据过滤**: 发送到云端 API 的内容建议经过 DLP 引擎扫描,避免泄露敏感信息\n- **Fallback 安全**: 智能回退到 Ollama 时数据不会离开本地,保障隐私\n- **模型来源**: 仅从官方渠道(Ollama Hub、Hugging Face)下载模型,避免使用不可信来源的模型文件\n\n---\n\n## 关键文件\n\n| 文件 | 职责 |\n| ------------------------------------------------------------------------ | --------------------------------- |\n| `desktop-app-vue/src/main/llm/llm-session.js` | LLM 会话管理与流式输出 |\n| `desktop-app-vue/src/main/llm/llm-providers.js` | 多提供商抽象层 |\n| `desktop-app-vue/src/main/llm/context-engineering.js` | Context Engineering KV-Cache 优化 |\n| `desktop-app-vue/src/main/rag/hybrid-search-engine.js` | Vector + BM25 混合检索 |\n| `desktop-app-vue/src/main/ai-engine/cowork/skills/multi-model-router.js` | 智能模型路由技能 |\n| `desktop-app-vue/src/main/llm/volcengine-model-selector.js` | 火山引擎模型选择器 |\n\n## 相关文档\n\n- [Computer Use 视觉自动化](/chainlesschain/computer-use)\n- [Skills 技能系统](/chainlesschain/skills)\n- [Cowork 多智能体协作](/chainlesschain/cowork)\n- [RAG 检索增强生成](/chainlesschain/knowledge-base)\n"}, "e8e7ba274b487ed491c04ec3359841a0e545debb": {"t": 1785809868.4392962, "n": 38210, "h": "e175591b48b0bbef", "c": "# AI模型配置\n\n> **版本: v0.37.6 | 14+云LLM提供商 | 多模态支持 | 智能模型路由 | Context Engineering**\n\nChainlessChain 支持多种AI模型,包括本地部署和云端API,满足不同的使用需求。桌面端默认使用火山引擎豆包(VolcEngine Doubao),支持 Ollama 本地模型自动回退,后端 AI Service 支持 14+ 云LLM提供商。\n\n## 概述\n\nAI 模型配置模块管理 ChainlessChain 的多模型接入与智能路由。系统集成 14+ LLM 提供商(Ollama、OpenAI、Anthropic、DeepSeek、火山引擎等),通过 Multi-Model Router 按任务复杂度自动选择最优模型,支持多模态视觉理解、KV-Cache Context Engineering 优化和云端失败自动回退本地模型。\n\n## 核心特性\n\n- 🤖 **14+ 提供商**: Ollama、OpenAI、Anthropic、DeepSeek、火山引擎等全面集成\n- 🎯 **智能路由**: Multi-Model Router 按任务复杂度自动选择最优模型\n- 👁️ **多模态支持**: LLaVA 本地 + 豆包 Vision + GPT-4V 视觉理解\n- ⚡ **Context Engineering**: KV-Cache 优化,60-85% 缓存命中率\n- 🔄 **智能回退**: 云端失败自动回退 Ollama 本地模型\n- 📊 **性能监控**: Token 追踪、成本分析、LLM Performance Dashboard\n\n## 系统架构\n\n```\n┌─────────────────────────────────────────────────────┐\n│ AI 模型管理层 │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌────────────┐ ┌────────────┐ ┌───────────────┐ │\n│ │ Multi-Model│ │ Context │ │ Token │ │\n│ │ Router │ │ Engineering│ │ Tracker │ │\n│ │ (智能路由) │ │ (KV-Cache) │ │ (用量监控) │ │\n│ └─────┬──────┘ └──────┬─────┘ └───────────────┘ │\n│ │ │ │\n│ ┌─────▼────────────────▼──────────────────────┐ │\n│ │ LLM Provider Abstraction │ │\n│ ├─────┬──────┬──────┬───────┬──────┬──────────┤ │\n│ │Ollama│OpenAI│Claude│DeepSeek│豆包 │Custom │ │\n│ └─────┴──────┴──────┴───────┴──────┴──────────┘ │\n│ │\n│ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │\n│ │ Embedding│ │ RAG │ │ Vision Model │ │\n│ │ Cache │ │ Hybrid │ │ (LLaVA/GPT-4V) │ │\n│ └──────────┘ └──────────┘ └─────────────────┘ │\n└─────────────────────────────────────────────────────┘\n```\n\n## 模型类型\n\n### 1. LLM (大语言模型)\n\n用于对话、推理和内容生成:\n\n- **本地模型**: 隐私保护,无需联网\n- **云端API**: 性能最强,需要费用\n\n### 2. 多模态视觉模型\n\n用于图像理解、视频理解、GUI自动化:\n\n- **本地模型**: LLaVA (Ollama) - 离线视觉理解\n- **云端API**: 豆包Vision、GPT-4V/4o、Claude Vision\n\n### 3. Embedding模型\n\n用于文本向量化,支持语义搜索:\n\n- **本地模型**: 离线可用\n- **云端API**: 更高精度\n\n### 4. 图像/视频生成模型\n\n用于AI创作:\n\n- **图像生成**: 豆包Seedream、DALL-E\n- **视频生成**: 豆包Seedance、PixelDance\n\n### 5. RAG (检索增强生成)\n\n结合向量检索和LLM,提供知识库问答。混合搜索引擎支持 Vector + BM25 + RRF融合。\n\n## 支持的提供商总览\n\n| 提供商 | 类型 | 桌面端 | 后端AI Service | Function Calling |\n| -------------------------- | ---- | ----------- | -------------- | ---------------- |\n| **Ollama** | 本地 | ✅ | ✅ | - |\n| **火山引擎/豆包** | 云端 | ✅ (默认) | ✅ | ✅ |\n| **OpenAI** | 云端 | ✅ | ✅ | ✅ |\n| **Anthropic Claude** | 云端 | ✅ | - | - |\n| **DeepSeek** | 云端 | ✅ | ✅ | ✅ |\n| **通义千问 (DashScope)** | 云端 | - | ✅ | ✅ |\n| **智谱AI (GLM)** | 云端 | - | ✅ | ✅ |\n| **百度千帆** | 云端 | - | ✅ | - |\n| **腾讯混元** | 云端 | - | ✅ | - |\n| **讯飞星火** | 云端 | - | ✅ | - |\n| **MiniMax** | 云端 | - | ✅ | - |\n| **Moonshot/Kimi** | 云端 | ✅ (Custom) | - | - |\n| **硅基流动 (SiliconFlow)** | 云端 | ✅ (Custom) | - | - |\n| **零一万物 (Yi)** | 云端 | ✅ (Custom) | - | - |\n| **LLaVA** | 本地 | ✅ | - | - |\n| **自定义 (Custom)** | 云端 | ✅ | - | - |\n\n## 本地LLM部署\n\n### PC端部署\n\n#### 使用Ollama(推荐)\n\n```bash\n# 1. 安装Ollama\ncurl -fsSL https://ollama.ai/install.sh | sh\n\n# 或Windows\nwinget install Ollama.Ollama\n\n# 2. 启动Ollama服务\nollama serve\n\n# 3. 下载模型\nollama pull qwen2:7b # 通义千问7B\nollama pull llama3:8b # LLaMA3 8B\nollama pull phi3:mini # Phi-3 Mini 3.8B\nollama pull deepseek-coder:6.7b # DeepSeek Coder\nollama pull llava:7b # LLaVA视觉模型\n```\n\n#### 模型推荐\n\n| 模型 | 参数量 | 内存需求 | 速度 | 质量 | 适用场景 |\n| -------------- | ------ | --------- | ---------- | ---------- | -------- |\n| Phi-3 Mini | 3.8B | 8GB | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 低配PC |\n| Qwen2 | 7B | 16GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 通用推荐 |\n| LLaMA3 | 8B | 16GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 通用推荐 |\n| DeepSeek Coder | 6.7B | 16GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 代码生成 |\n| Qwen2 | 14B | 32GB | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 高性能PC |\n| LLaMA3 | 70B | 64GB+ GPU | ⭐⭐ | ⭐⭐⭐⭐⭐ | 专业用户 |\n| LLaVA | 7B | 16GB | ⭐⭐⭐ | ⭐⭐⭐⭐ | 视觉理解 |\n\n#### 配置Ollama\n\n在ChainlessChain中配置:\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"ollama\",\n \"llmConfig\": {\n \"baseURL\": \"http://localhost:11434\",\n \"model\": \"qwen2:7b\",\n \"temperature\": 0.7,\n \"maxTokens\": 2000,\n \"contextWindow\": 8192\n }\n }\n}\n```\n\n#### Docker部署(可选)\n\n```yaml\n# docker-compose.yml\nversion: \"3.8\"\n\nservices:\n ollama:\n image: ollama/ollama:latest\n container_name: chainlesschain-llm\n ports:\n - \"11434:11434\"\n volumes:\n - ./ollama_data:/root/.ollama\n environment:\n - OLLAMA_HOST=0.0.0.0\n deploy:\n resources:\n reservations:\n devices:\n - driver: nvidia\n count: all\n capabilities: [gpu]\n```\n\n启动:\n\n```bash\ndocker-compose up -d\ndocker exec -it chainlesschain-llm ollama pull qwen2:7b\n```\n\n### 移动端部署\n\n#### Android\n\n使用MLC LLM运行量化模型:\n\n```kotlin\n// 1. 添加依赖\ndependencies {\n implementation 'ai.mlc:mlc-llm-android:0.1.0'\n}\n\n// 2. 初始化模型\nclass LocalLLM(context: Context) {\n private val mlcEngine = MLCEngine(context)\n\n suspend fun initialize() {\n // 下载量化模型(约1-2GB)\n val modelPath = downloadModel(\"MiniCPM-2B-Q4\")\n mlcEngine.loadModel(modelPath)\n }\n\n suspend fun chat(prompt: String): String {\n return mlcEngine.generate(prompt, maxTokens = 512)\n }\n}\n```\n\n推荐模型:\n\n- **MiniCPM-2B**: 2.4B参数,移动端最优\n- **Gemma-2B**: 2B参数,速度快\n- **Phi-3 Mini**: 3.8B参数,质量好\n\n#### iOS\n\n使用Core ML:\n\n```swift\nimport CoreML\n\nclass LocalLLM {\n private var model: MLModel?\n\n func initialize() {\n // 加载.mlpackage模型\n let modelURL = Bundle.main.url(\n forResource: \"MiniCPM-2B\",\n withExtension: \"mlpackage\"\n )!\n model = try? MLModel(contentsOf: modelURL)\n }\n\n func generate(prompt: String) async -> String {\n let input = MLDictionary([\"input_text\": prompt])\n let output = try? model?.prediction(from: input)\n return output?[\"generated_text\"] as? String ?? \"\"\n }\n}\n```\n\n## 云端API配置\n\n### 火山引擎/豆包(默认提供商)\n\nChainlessChain 桌面端默认使用火山引擎豆包系列模型。\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"volcengine\",\n \"llmConfig\": {\n \"apiKey\": \"your-api-key\",\n \"baseURL\": \"https://ark.cn-beijing.volces.com/api/v3\",\n \"model\": \"doubao-seed-1-6-251015\",\n \"embeddingModel\": \"doubao-embedding-text-240715\"\n }\n }\n}\n```\n\n#### 文本生成模型\n\n| 模型 | 上下文 | 费用 (输入/输出 ¥/百万tokens) | 特点 |\n| ------------------------ | ------ | ----------------------------- | ------------------------ |\n| doubao-seed-1.6 | 256K | ¥0.8 / ¥2 | 主力模型,深度思考,推荐 |\n| doubao-seed-1.6-thinking | 256K | ¥1 / ¥2.5 | 强制思考模式 |\n| doubao-seed-1.6-flash | 256K | ¥0.4 / ¥1 | 快速响应,低延迟 |\n| doubao-seed-1.6-lite | 256K | ¥0.3 / ¥0.8 | 最低成本 |\n| doubao-pro-32k | 32K | ¥0.8 / ¥2 | 专业版 |\n| doubao-seed-code | 128K | ¥0.6 / ¥1.8 | 代码生成专用 |\n\n#### 视觉模型\n\n| 模型 | 上下文 | 费用 | 特点 |\n| ---------------------- | ------ | -------------------- | ----------------------------- |\n| doubao-seed-1.6-vision | 256K | ¥2.6/¥7.8/百万tokens | 视觉深度思考,GUI Agent,推荐 |\n| doubao-1.5-vision-pro | 128K | ¥2/¥6/百万tokens | 专业视觉理解 |\n| doubao-1.5-vision-lite | 64K | ¥1/¥3/百万tokens | 轻量视觉 |\n| doubao-1.5-ui-tars | - | ¥2/¥6/百万tokens | GUI自动化专用 |\n\n#### 图像/视频生成模型\n\n| 模型 | 费用 | 特点 |\n| ----------------------- | ------------- | ------------------------------ |\n| doubao-seedream-4.5 | ¥0.08-0.15/张 | 最新图像生成,推荐 |\n| doubao-seedream-4.0 | ¥0.06-0.12/张 | 上一代图像生成 |\n| doubao-seededit-3.0 | ¥0.05/张 | 图像编辑(背景移除、光线调整) |\n| doubao-seedance-1.5-pro | ¥0.3/秒 | 专业视频生成,推荐 |\n| doubao-pixeldance | ¥0.3/秒 | 高动态视频生成 |\n\n#### Embedding模型\n\n| 模型 | 维度 | 费用 | 特点 |\n| ----------------------- | ---- | ---------------- | ---------------- |\n| doubao-embedding-large | 2048 | ¥0.2/百万tokens | 高精度检索,推荐 |\n| doubao-embedding | 1024 | ¥0.15/百万tokens | 标准,中英文 |\n| doubao-embedding-vision | 1536 | ¥0.3/百万tokens | 图像向量化 |\n\n#### 专用模型\n\n| 模型 | 费用 | 特点 |\n| ----------------------- | -------------------- | ---------- |\n| doubao-seed-translation | ¥0.5/¥1.5/百万tokens | 翻译专用 |\n| doubao-seed3d-1.0 | ¥1.5/个 | 3D模型生成 |\n\n---\n\n### DeepSeek\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"deepseek\",\n \"llmConfig\": {\n \"apiKey\": \"sk-...\",\n \"baseURL\": \"https://api.deepseek.com/v1\",\n \"model\": \"deepseek-chat\"\n }\n }\n}\n```\n\n#### 支持的模型\n\n| 模型 | 费用 (输入/输出) | 上下文 | 特点 |\n| ----------------- | ------------------------- | ------ | -------------------- |\n| deepseek-chat | $0.14 / $0.28 / 1M tokens | 128K | 通用对话,性价比极高 |\n| deepseek-coder | $0.14 / $0.28 / 1M tokens | 128K | 代码生成专用 |\n| deepseek-reasoner | $0.55 / $2.19 / 1M tokens | 128K | 深度推理(R1) |\n\n> DeepSeek 支持 Function Calling,桌面端和后端均已集成。\n\n---\n\n### OpenAI\n\n#### 注册和获取API Key\n\n```\n1. 访问 https://platform.openai.com\n2. 注册账号\n3. API Keys → Create new secret key\n4. 复制保存API Key (sk-...)\n```\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"openai\",\n \"llmConfig\": {\n \"apiKey\": \"sk-...\",\n \"model\": \"gpt-4o\",\n \"baseURL\": \"https://api.openai.com/v1\",\n \"temperature\": 0.7,\n \"maxTokens\": 2000\n }\n }\n}\n```\n\n#### 支持的模型\n\n| 模型 | 费用 (输入/输出) | 上下文 | 特点 |\n| ----------- | ------------------------- | ------ | ---------------- |\n| gpt-4o | $2.50 / $10 / 1M tokens | 128K | 最新旗舰,多模态 |\n| gpt-4o-mini | $0.15 / $0.60 / 1M tokens | 128K | 快速便宜 |\n| o1 | $15 / $60 / 1M tokens | 200K | 推理模型 |\n| o3-mini | $1.10 / $4.40 / 1M tokens | 200K | 推理模型(轻量) |\n| gpt-4-turbo | $10 / $30 / 1M tokens | 128K | 长上下文 |\n\n---\n\n### Anthropic Claude\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"anthropic\",\n \"llmConfig\": {\n \"apiKey\": \"sk-ant-...\",\n \"model\": \"claude-sonnet-4-5-20250929\",\n \"temperature\": 0.7,\n \"maxTokens\": 4096\n }\n }\n}\n```\n\n#### 模型对比\n\n| 模型 | 费用 | 上下文 | 特点 |\n| -------------------------- | --------------- | ------ | ----------------------------------------------------------------------- |\n| claude-opus-4-6 | $5 / $25 / 1M | 200K | 最强推理和编码(Opus 4.5+ 已降价;$15/$75 仅适用已退役的 Opus 4 / 4.1) |\n| claude-sonnet-4-5-20250929 | $3 / $15 / 1M | 200K | 平衡性能 |\n| claude-haiku-4-5-20251001 | $0.80 / $4 / 1M | 200K | 快速便宜 |\n\n---\n\n### 国内大模型\n\n#### 通义千问 (DashScope)\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"qwen\",\n \"llmConfig\": {\n \"apiKey\": \"sk-...\",\n \"model\": \"qwen-max\",\n \"baseURL\": \"https://dashscope.aliyuncs.com/api/v1\"\n }\n }\n}\n```\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=dashscope\nDASHSCOPE_API_KEY=sk-...\nDASHSCOPE_MODEL=qwen-turbo\n```\n\n#### 智谱AI (ChatGLM)\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"zhipu\",\n \"llmConfig\": {\n \"apiKey\": \"your-zhipu-key\",\n \"model\": \"glm-4\"\n }\n }\n}\n```\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=zhipu\nZHIPU_API_KEY=your-key\nZHIPU_MODEL=glm-4\n```\n\n> 智谱AI 支持 Function Calling。\n\n#### 百度千帆 (文心一言)\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"ernie\",\n \"llmConfig\": {\n \"apiKey\": \"your_key:secret\",\n \"model\": \"ERNIE-Bot-4\"\n }\n }\n}\n```\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=qianfan\nQIANFAN_API_KEY=ak:sk\nQIANFAN_MODEL=ERNIE-Bot-turbo\n```\n\n#### 腾讯混元\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=hunyuan\nHUNYUAN_API_KEY=your-key\nHUNYUAN_MODEL=hunyuan-lite\n```\n\n> 使用 OpenAI 兼容接口 (`https://api.hunyuan.cloud.tencent.com/v1`)。\n\n#### 讯飞星火\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=spark\nSPARK_API_KEY=app_id:api_key:api_secret\nSPARK_MODEL=spark-lite\n```\n\n> 使用 OpenAI 兼容接口 (`https://spark-api-open.xf-yun.com/v1`)。\n\n#### MiniMax\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=minimax\nMINIMAX_API_KEY=your-key\nMINIMAX_MODEL=abab5.5-chat\n```\n\n> 使用 OpenAI 兼容接口 (`https://api.minimax.chat/v1`)。\n\n#### Moonshot/Kimi\n\n通过桌面端 Custom 提供商或 OpenAI 兼容接口接入:\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"custom\",\n \"llmConfig\": {\n \"apiKey\": \"your-moonshot-key\",\n \"baseURL\": \"https://api.moonshot.cn/v1\",\n \"model\": \"moonshot-v1-8k\",\n \"name\": \"Moonshot\"\n }\n }\n}\n```\n\n支持模型:`moonshot-v1-8k`, `moonshot-v1-32k`, `moonshot-v1-128k`(长上下文)\n\n#### 硅基流动 (SiliconFlow)\n\n聚合多种开源模型,价格极低:\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"custom\",\n \"llmConfig\": {\n \"apiKey\": \"your-siliconflow-key\",\n \"baseURL\": \"https://api.siliconflow.cn/v1\",\n \"model\": \"Qwen/Qwen2.5-72B-Instruct\",\n \"name\": \"SiliconFlow\"\n }\n }\n}\n```\n\n支持模型:`Qwen2-7B`, `Qwen2.5-72B`, `DeepSeek-V2.5` 等,费用低至 ¥0.7/百万tokens\n\n#### 零一万物 (Yi)\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"custom\",\n \"llmConfig\": {\n \"apiKey\": \"your-yi-key\",\n \"baseURL\": \"https://api.lingyiwanwu.com/v1\",\n \"model\": \"yi-large\",\n \"name\": \"Yi\"\n }\n }\n}\n```\n\n---\n\n## 多模态视觉模型\n\n### LLaVA 本地视觉模型\n\nChainlessChain 内置 LLaVA 客户端,支持本地多模态视觉理解:\n\n```bash\n# 安装LLaVA模型\nollama pull llava:7b\n```\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"visionModel\": \"llava:7b\",\n \"visionConfig\": {\n \"baseURL\": \"http://localhost:11434\",\n \"timeout\": 300000,\n \"maxImageSize\": 5242880\n }\n }\n}\n```\n\n支持的图片格式:`.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.bmp`\n\n### 云端视觉模型\n\n| 提供商 | 模型 | 特点 |\n| --------- | ---------------------- | ----------------------- |\n| 火山引擎 | doubao-seed-1.6-vision | 视觉深度思考,GUI Agent |\n| OpenAI | gpt-4o | 图像+视频理解 |\n| Anthropic | claude-sonnet-4-5 | 图像理解 |\n\n> Computer Use 功能集成了 Vision AI,支持自动截图+视觉分析实现GUI自动化。详见 [Computer Use →](/chainlesschain/computer-use)\n\n---\n\n## 智能模型路由\n\n### Multi-Model Router 技能\n\nChainlessChain 内置智能模型路由技能 (`/multi-model-router`),根据任务复杂度自动选择最优模型:\n\n```\n/multi-model-router --route fix a typo in README.md\n→ Complexity: 1/10. Recommended: haiku (fastest, cheapest).\n\n/multi-model-router --route refactor the entire authentication system across 15 files\n→ Complexity: 9/10. Recommended: opus (strongest reasoning).\n```\n\n#### 模型能力矩阵\n\n| 模型 | 推理 | 编码 | 速度 | 成本 | 适用场景 |\n| --------------- | ---- | ---- | ---- | ---- | ------------------- |\n| opus | 10 | 9 | 3 | 10 | 架构设计、复杂调试 |\n| sonnet | 8 | 9 | 7 | 5 | 代码生成、重构 |\n| haiku | 5 | 6 | 10 | 1 | 简单编辑、格式化 |\n| gpt-4o | 9 | 8 | 6 | 7 | 通用任务 |\n| gpt-4o-mini | 6 | 6 | 9 | 2 | 快速问答、翻译 |\n| qwen2 | 7 | 7 | 8 | 0 | 本地推理(免费) |\n| doubao-seed-1.6 | 8 | 8 | 7 | 2 | 通用+深度思考 |\n| deepseek-chat | 8 | 9 | 7 | 1 | 代码+推理(极低成本) |\n\n### 火山引擎智能模型选择器\n\n桌面端内置 VolcEngine Model Selector,支持按任务类型自动选择豆包模型:\n\n```javascript\n// 任务类型 → 自动选择最优模型\nconst taskTypes = {\n chat: \"doubao-seed-1.6\", // 通用对话\n long_context: \"doubao-seed-1.6\", // 长文本处理\n complex_reasoning: \"doubao-seed-1.6-thinking\", // 复杂推理\n image_understanding: \"doubao-seed-1.6-vision\", // 图像理解\n code_writing: \"doubao-seed-code\", // 代码编写\n fast_response: \"doubao-seed-1.6-flash\", // 快速响应\n cost_effective: \"doubao-seed-1.6-lite\", // 成本优化\n gui_automation: \"doubao-1.5-ui-tars\", // GUI自动化\n translation: \"doubao-seed-translation\", // 翻译\n};\n```\n\n### 自动回退机制\n\n后端 AI Service 具备智能 Fallback 机制:\n\n- 当配置的云LLM API Key 未设置或为空时,自动回退到 Ollama 本地模型\n- 确保服务始终可用,不因API配置缺失而中断\n\n---\n\n## Context Engineering (KV-Cache优化)\n\nChainlessChain 集成 Manus 风格的 Context Engineering 优化:\n\n### 核心优化\n\n- **静态/动态分离**: 将静态系统提示词放在对话开头,提高 KV-Cache 命中率 (60-85%)\n- **工具定义序列化**: 按名称确定性排序,避免缓存失效\n- **任务目标重述**: 在上下文中间重述目标,防止\"迷失在中间\"\n- **错误历史追踪**: 从过去的错误中学习\n- **可恢复压缩**: 保留引用以便后续恢复\n- **Token估算**: 中英文自动检测\n\n### 配置\n\n```json\n{\n \"ai\": {\n \"contextEngineering\": {\n \"enabled\": true,\n \"kvCacheOptimization\": true,\n \"toolMasking\": true,\n \"taskTracking\": true,\n \"maxContextTokens\": 128000\n }\n }\n}\n```\n\n> 详见 Context Engineering IPC (17个处理器) 的完整配置。\n\n---\n\n## Embedding模型配置\n\n### 本地Embedding\n\n#### 使用sentence-transformers\n\n```python\nfrom sentence_transformers import SentenceTransformer\n\n# PC端 - 大模型\nmodel = SentenceTransformer('BAAI/bge-large-zh-v1.5')\n\n# 移动端 - 小模型\nmodel_mobile = SentenceTransformer('BAAI/bge-small-zh-v1.5')\n\n# 向量化\nembeddings = model.encode(['文本1', '文本2'])\n```\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"embeddingProvider\": \"local\",\n \"embeddingConfig\": {\n \"model\": \"BAAI/bge-large-zh-v1.5\",\n \"dimensions\": 1024,\n \"maxBatchSize\": 32,\n \"device\": \"cuda\"\n }\n }\n}\n```\n\n### 云端Embedding\n\n#### OpenAI Embeddings\n\n```json\n{\n \"ai\": {\n \"embeddingProvider\": \"openai\",\n \"embeddingConfig\": {\n \"apiKey\": \"sk-...\",\n \"model\": \"text-embedding-3-small\",\n \"dimensions\": 1536\n }\n }\n}\n```\n\n费用: $0.02 / 1M tokens (text-embedding-3-small)\n\n#### 火山引擎 Embedding\n\n```json\n{\n \"ai\": {\n \"embeddingProvider\": \"volcengine\",\n \"embeddingConfig\": {\n \"apiKey\": \"your-api-key\",\n \"model\": \"doubao-embedding-text-240715\",\n \"dimensions\": 1024\n }\n }\n}\n```\n\n费用: ¥0.15 / 百万tokens\n\n---\n\n## RAG配置\n\n### 基本配置\n\n```json\n{\n \"ai\": {\n \"ragEnabled\": true,\n \"ragConfig\": {\n \"topK\": 5,\n \"minScore\": 0.7,\n \"reranking\": true,\n \"contextWindow\": 4000,\n \"chunkSize\": 500,\n \"chunkOverlap\": 50\n }\n }\n}\n```\n\n### 高级配置\n\n#### 混合检索\n\nChainlessChain 内置 Hybrid Search Engine,支持 Vector + BM25 双路检索 + RRF融合:\n\n```json\n{\n \"ragConfig\": {\n \"retrievalMode\": \"hybrid\",\n \"vectorWeight\": 0.6,\n \"keywordWeight\": 0.4,\n \"fusion\": \"rrf\"\n }\n}\n```\n\n- **Vector Search**: 语义相似度,通过 RAG Manager Embedding\n- **BM25 Search**: Okapi BM25 算法,中英文分词器\n- **RRF Fusion**: Reciprocal Rank Fusion 结果合并\n- **性能**: <20ms 搜索延迟,并行执行\n\n#### 重排序\n\n```json\n{\n \"ragConfig\": {\n \"reranking\": true,\n \"rerankerModel\": \"BAAI/bge-reranker-large\",\n \"rerankerTopK\": 3\n }\n}\n```\n\n---\n\n## 查询模板\n\n### 创建模板\n\n```\n设置 → AI → 查询模板 → 新建\n\n模板信息:\n- 名称: 编程助手\n- 关联知识库: 技术文档、代码笔记\n- LLM模型: qwen2:7b\n- RAG模型: bge-large-zh-v1.5\n- 系统提示词: (见下方)\n- Temperature: 0.3\n- Max Tokens: 3000\n```\n\n### 系统提示词示例\n\n#### 通用问答\n\n```\n你是一个智能助手,基于用户的个人知识库回答问题。\n请遵循以下原则:\n1. 只使用检索到的知识库内容回答\n2. 如果知识库中没有相关信息,明确告知用户\n3. 提供详细准确的答案\n4. 必要时引用原文\n```\n\n#### 编程助手\n\n```\n你是一个专业的编程助手,帮助用户解决编程问题。\n知识库包含用户的技术笔记和代码片段。\n\n回答要求:\n1. 基于知识库中的代码示例\n2. 提供完整可运行的代码\n3. 解释代码逻辑\n4. 指出潜在问题\n5. 建议最佳实践\n```\n\n#### 文案优化\n\n```\n你是一个专业的文案编辑,帮助用户优化文字表达。\n\n任务:\n1. 分析用户的草稿\n2. 提出改进建议\n3. 提供优化后的版本\n4. 保持原意,提升表达质量\n```\n\n---\n\n## LLM性能监控\n\nChainlessChain 内置 LLM Performance Dashboard(路由: `#/llm/performance`),提供全面的模型使用分析:\n\n### 监控指标\n\n- **Token 使用量**: 输入/输出 Token 统计,每日趋势\n- **成本分析**: 各提供商费用追踪,成本预测\n- **响应延迟**: 首 Token 时间、完整响应时间分布\n- **缓存命中率**: Embedding 缓存、Response 缓存效果\n- **模型对比**: 不同模型的性能/成本对比\n\n### 配置\n\n```json\n{\n \"ai\": {\n \"tokenTracking\": {\n \"enabled\": true,\n \"budgetAlert\": {\n \"daily\": 100000,\n \"monthly\": 2000000\n }\n }\n }\n}\n```\n\n> 详见 [LLM Performance Dashboard 文档](https://github.com/chainlesschain/chainlesschain/blob/main/docs/features/LLM_PERFORMANCE_DASHBOARD.md)\n\n---\n\n## 性能优化\n\n### 缓存策略\n\n```json\n{\n \"ai\": {\n \"cacheEnabled\": true,\n \"cacheConfig\": {\n \"embeddingCache\": true,\n \"embeddingCacheTTL\": 86400,\n \"llmResponseCache\": false,\n \"maxCacheSize\": 1000\n }\n }\n}\n```\n\nEmbedding 缓存使用 SQLite 存储,避免重复计算向量。\n\n### 批处理\n\n```python\n# 批量向量化\ntexts = ['文本1', '文本2', ..., '文本100']\n\n# 不推荐:逐个处理\nembeddings = [model.encode(text) for text in texts]\n\n# 推荐:批处理\nembeddings = model.encode(texts, batch_size=32)\n```\n\n### GPU加速\n\n```json\n{\n \"ai\": {\n \"device\": \"cuda\",\n \"gpuMemoryFraction\": 0.8,\n \"precision\": \"fp16\"\n }\n}\n```\n\n---\n\n## 成本优化\n\n### 混合部署策略\n\n```typescript\nclass AIRouter {\n async route(query: string, context: any) {\n // 1. 简单问题 → 本地小模型\n if (context.length < 1000) {\n return this.localLLM.generate(query);\n }\n\n // 2. 复杂问题 → 本地大模型\n if (this.localLargeModel.available && !this.needHighQuality(query)) {\n return this.localLargeModel.generate(query);\n }\n\n // 3. 高质量要求 → 云端API\n return this.cloudAPI.generate(query);\n }\n}\n```\n\n### Token使用优化\n\n```typescript\n// 1. 精简提示词\nconst systemPrompt = \"你是助手,简洁回答。\"; // 而非长篇大论\n\n// 2. 限制上下文\nconst context = relevantDocs.slice(0, 3); // 只用最相关的3个文档\n\n// 3. 控制输出长度\nconst maxTokens = 500; // 根据需求调整\n\n// 4. 使用Context Engineering优化KV-Cache命中率\n// 静态内容前置,动态内容后置\n```\n\n### 成本对比\n\n| 方案 | 月成本估算 | 适用场景 |\n| ---------------------- | ---------- | --------------------- |\n| 纯本地 (Ollama) | ¥0 (电费) | 隐私优先、离线场景 |\n| 豆包 Seed 1.6 Lite | ¥5-50 | 日常对话、性价比首选 |\n| DeepSeek Chat | ¥5-30 | 代码+推理、极致性价比 |\n| GPT-4o-mini | ¥10-100 | 通用任务 |\n| GPT-4o / Claude Sonnet | ¥50-500 | 专业开发 |\n| GPT-o1 / Claude Opus | ¥200-2000 | 复杂推理 |\n\n---\n\n## 模型评估\n\n### 质量评估\n\n```python\n# 评估检索质量\ndef evaluate_retrieval(test_queries, ground_truth):\n scores = {\n 'precision': [],\n 'recall': [],\n 'ndcg': []\n }\n\n for query, truth in zip(test_queries, ground_truth):\n retrieved = rag.retrieve(query)\n scores['precision'].append(precision(retrieved, truth))\n scores['recall'].append(recall(retrieved, truth))\n scores['ndcg'].append(ndcg(retrieved, truth))\n\n return scores\n\n# 评估生成质量\ndef evaluate_generation(test_cases):\n scores = []\n for query, expected_answer in test_cases:\n generated = llm.generate(query)\n score = similarity(generated, expected_answer)\n scores.append(score)\n return np.mean(scores)\n```\n\n### 性能监控\n\n```\n设置 → AI → 性能监控\n\n指标:\n- 平均响应时间\n- 每日Token消耗\n- 检索精度\n- 用户满意度(评分)\n- KV-Cache命中率\n\n图表:\n- 每日使用量趋势\n- 不同模型的成本对比\n- 响应速度分布\n- 缓存命中率趋势\n```\n\n---\n\n## 使用示例\n\n### Ollama 本地模型配置与使用\n\n```bash\n# 安装并启动 Ollama\nollama serve\n\n# 下载推荐模型\nollama pull qwen2:7b\n\n# 通过 CLI 测试模型连通性\nchainlesschain llm test\nchainlesschain llm models # 列出已安装模型\n```\n\n### 切换 LLM 提供商\n\n```bash\n# 切换到 DeepSeek(高性价比云端)\nchainlesschain llm switch deepseek\nchainlesschain config set llm.apiKey sk-xxx\n\n# 切换到 Ollama 本地模型\nchainlesschain llm switch ollama\n\n# 查看当前提供商状态\nchainlesschain llm providers\n```\n\n### 在对话中使用不同模型\n\n```bash\n# 单次问答(使用当前配置的模型)\nchainlesschain ask \"解释量子计算的基本原理\"\n\n# 交互式对话(支持流式输出)\nchainlesschain chat\n\n# Agent 模式(146 技能 + Plan Mode)\nchainlesschain agent\n```\n\n---\n\n## 故障排查\n\n### 本地模型加载失败\n\n**常见问题**:\n\n```\nError: Failed to load model\n\n原因:\n1. 内存不足\n2. 模型文件损坏\n3. 依赖缺失\n\n解决:\n1. 检查内存: htop / 任务管理器\n2. 重新下载模型: ollama pull qwen2:7b\n3. 检查日志: ~/.ollama/logs/\n```\n\n### API调用失败\n\n**检查清单**:\n\n```\n□ API Key是否正确\n□ 账户余额是否充足\n□ 网络连接是否正常\n□ 是否超过速率限制\n□ baseURL是否正确\n□ 提供商服务是否正常\n```\n\n### 智能Fallback失效\n\n如果云端API失败但没有自动回退到Ollama:\n\n```\n检查:\n1. Ollama服务是否运行: curl http://localhost:11434/api/tags\n2. 本地模型是否已下载: ollama list\n3. 配置中fallback是否启用\n```\n\n### 响应速度慢\n\n**优化步骤**:\n\n```\n1. 使用更快的模型(doubao-seed-1.6-flash、Phi-3、Gemma)\n2. 减少上下文长度\n3. 启用GPU加速\n4. 使用量化模型(Q4, Q8)\n5. 启用Context Engineering的KV-Cache优化\n6. 考虑使用云端API\n```\n\n---\n\n## 常见问题\n\n### 本地模型vs云端API,如何选择?\n\n```\n本地模型适合:\n✓ 隐私敏感数据\n✓ 离线使用场景\n✓ 长期大量使用(成本低)\n✓ 实时响应要求\n\n云端API适合:\n✓ 对质量要求极高\n✓ 使用量不大\n✓ 无本地算力\n✓ 需要最新功能\n```\n\n### 如何降低成本?\n\n```\n策略:\n1. 本地模型为主,云端为辅\n2. 使用Multi-Model Router按任务复杂度路由\n3. 缓存常见问题的答案(Embedding Cache + Response Cache)\n4. 优化提示词,减少Token消耗\n5. 使用Context Engineering提升KV-Cache命中率\n6. 使用更便宜的模型(DeepSeek、豆包Lite、GPT-4o-mini、Haiku)\n7. 批处理请求\n```\n\n### 模型推荐?\n\n```\n个人使用:\n- PC: Qwen2:7B (本地) + 豆包Seed 1.6 Lite (云端,最低成本)\n- 移动: MiniCPM-2B (本地)\n- 云端首选: DeepSeek Chat (性价比之王)\n\n专业开发者:\n- PC: LLaMA3:70B (本地) + Claude Sonnet 4.5 (云端)\n- 代码: DeepSeek Coder + doubao-seed-code\n- 推理: DeepSeek Reasoner / Claude Opus 4.6\n\n企业:\n- 自建: LLaMA3:70B 集群\n- 国内云端: 豆包Seed 1.6 (默认) / 通义千问Max\n- 国际云端: GPT-4o / Claude Sonnet 4.5\n- 合规审计: 配合Enterprise Audit系统\n```\n\n### 为什么默认使用火山引擎豆包?\n\n```\n原因:\n1. 国内访问稳定,无需代理\n2. 256K超长上下文窗口\n3. 价格极具竞争力(¥0.3-2/百万tokens)\n4. 支持深度思考模式\n5. 完整多模态能力(视觉/图像/视频/3D)\n6. 原生Function Calling支持\n7. 中文能力优秀\n```\n\n---\n\n## 已实现功能\n\n- [x] 14+ 云LLM提供商集成(Ollama、OpenAI、Anthropic、DeepSeek、火山引擎、通义千问、智谱AI、百度千帆、腾讯混元、讯飞星火、MiniMax等)\n- [x] 多模态视觉模型(LLaVA本地 + 豆包Vision + GPT-4V + Claude Vision)\n- [x] 智能模型路由(Multi-Model Router技能)\n- [x] Context Engineering KV-Cache优化\n- [x] LLM Performance Dashboard(Token追踪、成本分析)\n- [x] 混合检索引擎(Vector + BM25 + RRF融合)\n- [x] Embedding缓存(SQLite存储)\n- [x] 响应缓存和流式输出\n- [x] 火山引擎智能模型选择器(按任务类型自动选择)\n- [x] 图像生成(豆包Seedream 3.0/4.0/4.5)\n- [x] 视频生成(豆包Seedance、PixelDance)\n- [x] 图像编辑(豆包SeedEdit)\n- [x] 3D模型生成(doubao-seed3d-1.0)\n- [x] GUI自动化模型(doubao-1.5-ui-tars)\n- [x] 翻译专用模型(doubao-seed-translation)\n- [x] 智能Fallback(云端失败自动回退Ollama)\n- [x] API Key加密存储(AES-256-GCM)\n\n## 未来规划\n\n- [x] 模型微调支持(LoRA/QLoRA本地微调)\n- [x] 语音模型集成(Whisper语音识别 + TTS合成)\n- [x] 联邦学习(分布式模型训练,保护隐私)\n- [x] 更多模型支持(Google Gemini、Mistral、零一万物Yi等)\n- [x] 本地模型量化工具(一键GGUF/GPTQ量化)\n- [x] 模型性能基准测试(自动化评估框架)\n- [x] 多模型协作(Architect+Editor双模型模式)\n- [x] 长期记忆增强(结合Permanent Memory的个性化模型)\n\n## 配置参考\n\n### 完整 LLM 提供商配置结构\n\n```js\n// desktop-app-vue/src/main/config/unified-config-manager.js\n// .chainlesschain/config.json — ai 段完整字段\n{\n \"ai\": {\n // 主提供商选择: \"ollama\" | \"volcengine\" | \"openai\" | \"anthropic\" |\n // \"deepseek\" | \"qwen\" | \"zhipu\" | \"ernie\" | \"custom\"\n \"llmProvider\": \"volcengine\",\n\n // 提供商连接参数 (与 llmProvider 对应)\n \"llmConfig\": {\n \"apiKey\": \"your-api-key\", // 云端提供商 API Key(AES-256-GCM 加密存储)\n \"baseURL\": \"https://ark.cn-beijing.volces.com/api/v3\", // 自定义端点\n \"model\": \"doubao-seed-1.6-251015\", // 文本生成主模型\n \"embeddingModel\": \"doubao-embedding-text-240715\", // Embedding 模型\n \"temperature\": 0.7, // 采样温度 (0.0–1.0)\n \"maxTokens\": 4096, // 单次最大输出 token\n \"contextWindow\": 131072 // 上下文窗口大小\n },\n\n // 视觉模型单独配置\n \"visionModel\": \"doubao-seed-1.6-vision\",\n \"visionConfig\": {\n \"baseURL\": \"http://localhost:11434\", // 本地 LLaVA 时填 Ollama 地址\n \"timeout\": 300000,\n \"maxImageSize\": 5242880 // 最大图片尺寸 (bytes)\n },\n\n // Embedding 提供商: \"local\" | \"openai\" | \"volcengine\"\n \"embeddingProvider\": \"volcengine\",\n \"embeddingConfig\": {\n \"apiKey\": \"your-api-key\",\n \"model\": \"doubao-embedding-text-240715\",\n \"dimensions\": 1024,\n \"maxBatchSize\": 32\n },\n\n // Context Engineering (KV-Cache 优化)\n \"contextEngineering\": {\n \"enabled\": true,\n \"kvCacheOptimization\": true, // 静态/动态内容分离\n \"toolMasking\": true, // 按激活技能过滤工具集\n \"taskTracking\": true, // 任务目标重述\n \"maxContextTokens\": 128000\n },\n\n // RAG 混合检索\n \"ragEnabled\": true,\n \"ragConfig\": {\n \"retrievalMode\": \"hybrid\", // \"vector\" | \"bm25\" | \"hybrid\"\n \"topK\": 5,\n \"minScore\": 0.7,\n \"vectorWeight\": 0.6,\n \"keywordWeight\": 0.4,\n \"fusion\": \"rrf\", // Reciprocal Rank Fusion\n \"reranking\": true,\n \"rerankerModel\": \"BAAI/bge-reranker-large\",\n \"contextWindow\": 4000,\n \"chunkSize\": 500,\n \"chunkOverlap\": 50\n },\n\n // Token 用量追踪与预算\n \"tokenTracking\": {\n \"enabled\": true,\n \"budgetAlert\": {\n \"daily\": 100000, // 日预算 token 上限\n \"monthly\": 2000000 // 月预算 token 上限\n }\n },\n\n // Embedding 响应缓存\n \"cacheEnabled\": true,\n \"cacheConfig\": {\n \"embeddingCache\": true,\n \"embeddingCacheTTL\": 86400, // 秒,默认 24h\n \"llmResponseCache\": false,\n \"maxCacheSize\": 1000\n },\n\n // GPU 加速 (Embedding / 本地推理)\n \"device\": \"cuda\", // \"cpu\" | \"cuda\" | \"mps\"\n \"gpuMemoryFraction\": 0.8,\n \"precision\": \"fp16\"\n }\n}\n```\n\n### 模型智能路由配置\n\n```js\n// llm-manager.js — LLM_CATEGORIES 与 Category Routing\n// 用于 resolveCategory(category, { skill }) 方法\n\n// 可选类别值\nconst LLM_CATEGORIES = {\n QUICK: \"quick\", // 补全/简单改写 → 优先本地 ollama\n DEEP: \"deep\", // 长上下文/架构 → 优先 anthropic / openai\n REASONING: \"reasoning\", // 推理密集 → 优先 deepseek / o1\n VISION: \"vision\", // 多模态 → 优先 gemini / gpt-4o\n CREATIVE: \"creative\", // 文案/UI创作 → 优先 anthropic\n EMBEDDING: \"embedding\", // 文本向量化 → 优先 volcengine / openai\n AUDIO: \"audio\", // 语音识别/TTS → 优先 openai whisper\n ASR: \"asr\", // 语音转文字 → 优先 openai / gemini\n AUDIO_ANALYSIS: \"audio-analysis\", // 节拍检测/音频特征 → 优先本地 ollama\n VIDEO_VLM: \"video-vlm\", // 视频理解 VLM → 优先 gemini / openai\n};\n\n// SKILL.md 中声明 model-hints 触发自动类别推断\n// capability: \"reasoning\" → REASONING\n// capability: \"transcription\" → ASR\n// capability: \"beat-detection\" → AUDIO_ANALYSIS\n// capability: \"video-review\" → VIDEO_VLM\n// context-window: \"large\" → DEEP\n```\n\n### 自定义 OpenAI 兼容端点\n\n```js\n// 适用于 Moonshot、SiliconFlow、零一万物、本地 vLLM 等 OpenAI 兼容服务\n{\n \"ai\": {\n \"llmProvider\": \"custom\",\n \"llmConfig\": {\n \"apiKey\": \"your-provider-key\",\n \"baseURL\": \"https://api.siliconflow.cn/v1\", // 替换为实际端点\n \"model\": \"Qwen/Qwen2.5-72B-Instruct\",\n \"name\": \"SiliconFlow\", // 显示名称\n \"temperature\": 0.7,\n \"maxTokens\": 4096\n }\n }\n}\n```\n\n---\n\n## 性能指标\n\n### 推理延迟(首 Token 时间,TTFT)\n\n| 模型 | 硬件条件 | 平均 TTFT | P95 TTFT | 备注 |\n| ------------------------ | ------------- | --------- | -------- | ---------------- |\n| Phi-3 Mini 3.8B (Q4) | CPU 16GB RAM | 320 ms | 680 ms | 低配 PC 推荐 |\n| Qwen2 7B (Q4) | CPU 16GB RAM | 580 ms | 1200 ms | 通用本地推荐 |\n| Qwen2 7B (Q4) | GPU RTX 3060 | 95 ms | 180 ms | GPU 加速 |\n| LLaMA3 8B (Q4) | CPU 16GB RAM | 620 ms | 1300 ms | - |\n| LLaMA3 8B (Q4) | GPU RTX 3060 | 110 ms | 210 ms | GPU 加速 |\n| LLaMA3 70B (Q4) | GPU A100 40GB | 280 ms | 520 ms | 专业服务器 |\n| doubao-seed-1.6-flash | 云端 API | 180 ms | 420 ms | 低延迟模型 |\n| doubao-seed-1.6 | 云端 API | 350 ms | 780 ms | 标准主力模型 |\n| doubao-seed-1.6-thinking | 云端 API | 1200 ms | 3500 ms | 深度思考模式 |\n| deepseek-chat | 云端 API | 280 ms | 650 ms | 高性价比 |\n| gpt-4o | 云端 API | 420 ms | 950 ms | OpenAI 旗舰 |\n| gpt-4o-mini | 云端 API | 190 ms | 420 ms | 快速便宜 |\n| claude-sonnet-4-5 | 云端 API | 380 ms | 820 ms | Anthropic 平衡版 |\n\n### Token 吞吐量(输出速率)\n\n| 模型 | 硬件条件 | 平均吞吐 (tokens/s) | 备注 |\n| --------------------- | ------------- | ------------------- | ------------ |\n| Phi-3 Mini 3.8B (Q4) | CPU 16GB RAM | 12–18 t/s | 低配 PC |\n| Qwen2 7B (Q4) | CPU 16GB RAM | 8–14 t/s | 通用本地 |\n| Qwen2 7B (Q4) | GPU RTX 3060 | 45–65 t/s | GPU 加速 |\n| LLaMA3 8B (Q4) | GPU RTX 3060 | 50–70 t/s | GPU 加速 |\n| LLaMA3 70B (Q4) | GPU A100 40GB | 28–40 t/s | 专业服务器 |\n| doubao-seed-1.6-flash | 云端 API | 80–120 t/s | 低延迟模型 |\n| doubao-seed-1.6 | 云端 API | 45–80 t/s | 标准主力模型 |\n| deepseek-chat | 云端 API | 50–90 t/s | 高性价比 |\n| gpt-4o-mini | 云端 API | 70–110 t/s | 快速便宜 |\n| gpt-4o | 云端 API | 40–65 t/s | OpenAI 旗舰 |\n\n### Context Engineering KV-Cache 命中率\n\n| 场景 | 命中率 | Token 节省 | 说明 |\n| -------------------------- | ------ | ---------- | ------------------------- |\n| 单轮对话(短提示词) | 25–40% | 15–25% | 系统提示词命中 |\n| 多轮对话(同会话) | 60–75% | 40–55% | 历史上下文命中 |\n| Agent 模式(工具定义稳定) | 70–85% | 50–65% | 工具定义 + 系统提示词命中 |\n| RAG 增强问答 | 55–70% | 35–50% | 文档块重复检索命中 |\n\n### Embedding 与 RAG 性能\n\n| 指标 | 本地 (BGE-large) | 云端 (doubao-embedding) | 云端 (OpenAI ada-3) |\n| -------------------- | ---------------- | ----------------------- | ------------------- |\n| 向量化延迟 (单条) | 8–15 ms (GPU) | 35–60 ms | 40–70 ms |\n| 批处理吞吐 (32条) | 180–280 条/s | 受 API 限速 | 受 API 限速 |\n| 混合检索延迟 | <20 ms | <20 ms | <20 ms |\n| 检索精度 (MRR@5) | 0.81 | 0.87 | 0.85 |\n| Embedding 缓存命中率 | — | 65–80% | 65–80% |\n\n---\n\n## 测试覆盖率\n\n### LLM 核心模块测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| -------------------------------------------------------------------------------- | ------------------------------------------------------- | ------ |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/llm-manager.test.js` | LLMManager 多提供商路由、chat()、fallback | 48 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/llm-manager-category-routing.test.js` | Category Routing (quick/deep/reasoning/vision/creative) | 26 |\n| ✅ `desktop-app-vue/tests/unit/llm/llm-manager-media-categories.test.js` | 媒体类别扩展 (asr/audio-analysis/video-vlm) | 25 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/token-tracker.test.js` | Token 用量统计、成本计算、预算告警 | 22 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/prompt-compressor.test.js` | 去重/截断/总结三层压缩策略 | 18 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/context-engineering.test.js` | KV-Cache 优化、工具过滤、任务追踪 | 31 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/volcengine-model-selector.test.js` | 火山引擎任务类型→模型自动选择 | 19 |\n\n### RAG 与 Embedding 测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| ---------------------------------------------------------------- | ----------------------------------- | ------ |\n| ✅ `desktop-app-vue/tests/unit/rag/hybrid-search-engine.test.js` | Vector + BM25 双路检索 + RRF 融合 | 34 |\n| ✅ `desktop-app-vue/tests/unit/rag/embedding-cache.test.js` | SQLite Embedding 缓存读写、TTL 过期 | 16 |\n| ✅ `desktop-app-vue/tests/unit/rag/rag-manager.test.js` | RAG 检索、重排序、上下文组装 | 27 |\n\n### 多模态与视觉模型测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| -------------------------------------------------------------------- | -------------------------------- | ------ |\n| ✅ `desktop-app-vue/tests/unit/llm/llava-client.test.js` | LLaVA 本地视觉模型调用、图片编码 | 14 |\n| ✅ `desktop-app-vue/tests/unit/ai-engine/multi-model-router.test.js` | 任务复杂度评分、模型推荐逻辑 | 21 |\n\n### 视频生成集成测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| ------------------------------------------------------------ | ------------------------------------------- | ------ |\n| ✅ `desktop-app-vue/tests/unit/llm/volcengine-video.test.js` | Seedance text-to-video 任务提交/轮询 (mock) | 18 |\n| ✅ `packages/cli/__tests__/video/video-generate.test.js` | CLI video 生成命令端到端 (mock API) | 12 |\n\n### CLI LLM 命令测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| ------------------------------------------------------ | --------------------------------------------- | ------ |\n| ✅ `packages/cli/__tests__/unit/llm-providers.test.js` | CLI 提供商切换、providers 列表 | 17 |\n| ✅ `packages/cli/__tests__/unit/llm-switch.test.js` | `chainlesschain llm switch` 持久化配置 | 11 |\n| ✅ `packages/cli/__tests__/unit/stream-router.test.js` | StreamRouter NDJSON 输出、provider/model 透传 | 24 |\n\n---\n\n## 安全考虑\n\n- **API Key 加密存储**: 所有 API Key 使用 AES-256-GCM 加密后存储在本地 `config.json` 中,不以明文保存\n- **本地推理隐私**: 使用 Ollama 本地模型时数据完全离线,不发送到任何外部服务器\n- **传输加密**: 云端 API 调用强制使用 HTTPS/TLS 加密传输\n- **密钥轮换**: 建议定期更换云端 API Key,旧 Key 及时在提供商控制台吊销\n- **预算控制**: 启用 Token 追踪和预算告警(`tokenTracking.budgetAlert`),防止意外超支\n- **敏感数据过滤**: 发送到云端 API 的内容建议经过 DLP 引擎扫描,避免泄露敏感信息\n- **Fallback 安全**: 智能回退到 Ollama 时数据不会离开本地,保障隐私\n- **模型来源**: 仅从官方渠道(Ollama Hub、Hugging Face)下载模型,避免使用不可信来源的模型文件\n\n---\n\n## 关键文件\n\n| 文件 | 职责 |\n| ------------------------------------------------------------------------ | --------------------------------- |\n| `desktop-app-vue/src/main/llm/llm-session.js` | LLM 会话管理与流式输出 |\n| `desktop-app-vue/src/main/llm/llm-providers.js` | 多提供商抽象层 |\n| `desktop-app-vue/src/main/llm/context-engineering.js` | Context Engineering KV-Cache 优化 |\n| `desktop-app-vue/src/main/rag/hybrid-search-engine.js` | Vector + BM25 混合检索 |\n| `desktop-app-vue/src/main/ai-engine/cowork/skills/multi-model-router.js` | 智能模型路由技能 |\n| `desktop-app-vue/src/main/llm/volcengine-model-selector.js` | 火山引擎模型选择器 |\n\n## 相关文档\n\n- [Computer Use 视觉自动化](/chainlesschain/computer-use)\n- [Skills 技能系统](/chainlesschain/skills)\n- [Cowork 多智能体协作](/chainlesschain/cowork)\n- [RAG 检索增强生成](/chainlesschain/knowledge-base)\n"}, "4db48500206ff6a789c53c8197933113b06a6097": {"t": 1785809903.7131426, "n": 38210, "h": "e175591b48b0bbef", "c": "# AI模型配置\n\n> **版本: v0.37.6 | 14+云LLM提供商 | 多模态支持 | 智能模型路由 | Context Engineering**\n\nChainlessChain 支持多种AI模型,包括本地部署和云端API,满足不同的使用需求。桌面端默认使用火山引擎豆包(VolcEngine Doubao),支持 Ollama 本地模型自动回退,后端 AI Service 支持 14+ 云LLM提供商。\n\n## 概述\n\nAI 模型配置模块管理 ChainlessChain 的多模型接入与智能路由。系统集成 14+ LLM 提供商(Ollama、OpenAI、Anthropic、DeepSeek、火山引擎等),通过 Multi-Model Router 按任务复杂度自动选择最优模型,支持多模态视觉理解、KV-Cache Context Engineering 优化和云端失败自动回退本地模型。\n\n## 核心特性\n\n- 🤖 **14+ 提供商**: Ollama、OpenAI、Anthropic、DeepSeek、火山引擎等全面集成\n- 🎯 **智能路由**: Multi-Model Router 按任务复杂度自动选择最优模型\n- 👁️ **多模态支持**: LLaVA 本地 + 豆包 Vision + GPT-4V 视觉理解\n- ⚡ **Context Engineering**: KV-Cache 优化,60-85% 缓存命中率\n- 🔄 **智能回退**: 云端失败自动回退 Ollama 本地模型\n- 📊 **性能监控**: Token 追踪、成本分析、LLM Performance Dashboard\n\n## 系统架构\n\n```\n┌─────────────────────────────────────────────────────┐\n│ AI 模型管理层 │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌────────────┐ ┌────────────┐ ┌───────────────┐ │\n│ │ Multi-Model│ │ Context │ │ Token │ │\n│ │ Router │ │ Engineering│ │ Tracker │ │\n│ │ (智能路由) │ │ (KV-Cache) │ │ (用量监控) │ │\n│ └─────┬──────┘ └──────┬─────┘ └───────────────┘ │\n│ │ │ │\n│ ┌─────▼────────────────▼──────────────────────┐ │\n│ │ LLM Provider Abstraction │ │\n│ ├─────┬──────┬──────┬───────┬──────┬──────────┤ │\n│ │Ollama│OpenAI│Claude│DeepSeek│豆包 │Custom │ │\n│ └─────┴──────┴──────┴───────┴──────┴──────────┘ │\n│ │\n│ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │\n│ │ Embedding│ │ RAG │ │ Vision Model │ │\n│ │ Cache │ │ Hybrid │ │ (LLaVA/GPT-4V) │ │\n│ └──────────┘ └──────────┘ └─────────────────┘ │\n└─────────────────────────────────────────────────────┘\n```\n\n## 模型类型\n\n### 1. LLM (大语言模型)\n\n用于对话、推理和内容生成:\n\n- **本地模型**: 隐私保护,无需联网\n- **云端API**: 性能最强,需要费用\n\n### 2. 多模态视觉模型\n\n用于图像理解、视频理解、GUI自动化:\n\n- **本地模型**: LLaVA (Ollama) - 离线视觉理解\n- **云端API**: 豆包Vision、GPT-4V/4o、Claude Vision\n\n### 3. Embedding模型\n\n用于文本向量化,支持语义搜索:\n\n- **本地模型**: 离线可用\n- **云端API**: 更高精度\n\n### 4. 图像/视频生成模型\n\n用于AI创作:\n\n- **图像生成**: 豆包Seedream、DALL-E\n- **视频生成**: 豆包Seedance、PixelDance\n\n### 5. RAG (检索增强生成)\n\n结合向量检索和LLM,提供知识库问答。混合搜索引擎支持 Vector + BM25 + RRF融合。\n\n## 支持的提供商总览\n\n| 提供商 | 类型 | 桌面端 | 后端AI Service | Function Calling |\n| -------------------------- | ---- | ----------- | -------------- | ---------------- |\n| **Ollama** | 本地 | ✅ | ✅ | - |\n| **火山引擎/豆包** | 云端 | ✅ (默认) | ✅ | ✅ |\n| **OpenAI** | 云端 | ✅ | ✅ | ✅ |\n| **Anthropic Claude** | 云端 | ✅ | - | - |\n| **DeepSeek** | 云端 | ✅ | ✅ | ✅ |\n| **通义千问 (DashScope)** | 云端 | - | ✅ | ✅ |\n| **智谱AI (GLM)** | 云端 | - | ✅ | ✅ |\n| **百度千帆** | 云端 | - | ✅ | - |\n| **腾讯混元** | 云端 | - | ✅ | - |\n| **讯飞星火** | 云端 | - | ✅ | - |\n| **MiniMax** | 云端 | - | ✅ | - |\n| **Moonshot/Kimi** | 云端 | ✅ (Custom) | - | - |\n| **硅基流动 (SiliconFlow)** | 云端 | ✅ (Custom) | - | - |\n| **零一万物 (Yi)** | 云端 | ✅ (Custom) | - | - |\n| **LLaVA** | 本地 | ✅ | - | - |\n| **自定义 (Custom)** | 云端 | ✅ | - | - |\n\n## 本地LLM部署\n\n### PC端部署\n\n#### 使用Ollama(推荐)\n\n```bash\n# 1. 安装Ollama\ncurl -fsSL https://ollama.ai/install.sh | sh\n\n# 或Windows\nwinget install Ollama.Ollama\n\n# 2. 启动Ollama服务\nollama serve\n\n# 3. 下载模型\nollama pull qwen2:7b # 通义千问7B\nollama pull llama3:8b # LLaMA3 8B\nollama pull phi3:mini # Phi-3 Mini 3.8B\nollama pull deepseek-coder:6.7b # DeepSeek Coder\nollama pull llava:7b # LLaVA视觉模型\n```\n\n#### 模型推荐\n\n| 模型 | 参数量 | 内存需求 | 速度 | 质量 | 适用场景 |\n| -------------- | ------ | --------- | ---------- | ---------- | -------- |\n| Phi-3 Mini | 3.8B | 8GB | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 低配PC |\n| Qwen2 | 7B | 16GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 通用推荐 |\n| LLaMA3 | 8B | 16GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 通用推荐 |\n| DeepSeek Coder | 6.7B | 16GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 代码生成 |\n| Qwen2 | 14B | 32GB | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 高性能PC |\n| LLaMA3 | 70B | 64GB+ GPU | ⭐⭐ | ⭐⭐⭐⭐⭐ | 专业用户 |\n| LLaVA | 7B | 16GB | ⭐⭐⭐ | ⭐⭐⭐⭐ | 视觉理解 |\n\n#### 配置Ollama\n\n在ChainlessChain中配置:\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"ollama\",\n \"llmConfig\": {\n \"baseURL\": \"http://localhost:11434\",\n \"model\": \"qwen2:7b\",\n \"temperature\": 0.7,\n \"maxTokens\": 2000,\n \"contextWindow\": 8192\n }\n }\n}\n```\n\n#### Docker部署(可选)\n\n```yaml\n# docker-compose.yml\nversion: \"3.8\"\n\nservices:\n ollama:\n image: ollama/ollama:latest\n container_name: chainlesschain-llm\n ports:\n - \"11434:11434\"\n volumes:\n - ./ollama_data:/root/.ollama\n environment:\n - OLLAMA_HOST=0.0.0.0\n deploy:\n resources:\n reservations:\n devices:\n - driver: nvidia\n count: all\n capabilities: [gpu]\n```\n\n启动:\n\n```bash\ndocker-compose up -d\ndocker exec -it chainlesschain-llm ollama pull qwen2:7b\n```\n\n### 移动端部署\n\n#### Android\n\n使用MLC LLM运行量化模型:\n\n```kotlin\n// 1. 添加依赖\ndependencies {\n implementation 'ai.mlc:mlc-llm-android:0.1.0'\n}\n\n// 2. 初始化模型\nclass LocalLLM(context: Context) {\n private val mlcEngine = MLCEngine(context)\n\n suspend fun initialize() {\n // 下载量化模型(约1-2GB)\n val modelPath = downloadModel(\"MiniCPM-2B-Q4\")\n mlcEngine.loadModel(modelPath)\n }\n\n suspend fun chat(prompt: String): String {\n return mlcEngine.generate(prompt, maxTokens = 512)\n }\n}\n```\n\n推荐模型:\n\n- **MiniCPM-2B**: 2.4B参数,移动端最优\n- **Gemma-2B**: 2B参数,速度快\n- **Phi-3 Mini**: 3.8B参数,质量好\n\n#### iOS\n\n使用Core ML:\n\n```swift\nimport CoreML\n\nclass LocalLLM {\n private var model: MLModel?\n\n func initialize() {\n // 加载.mlpackage模型\n let modelURL = Bundle.main.url(\n forResource: \"MiniCPM-2B\",\n withExtension: \"mlpackage\"\n )!\n model = try? MLModel(contentsOf: modelURL)\n }\n\n func generate(prompt: String) async -> String {\n let input = MLDictionary([\"input_text\": prompt])\n let output = try? model?.prediction(from: input)\n return output?[\"generated_text\"] as? String ?? \"\"\n }\n}\n```\n\n## 云端API配置\n\n### 火山引擎/豆包(默认提供商)\n\nChainlessChain 桌面端默认使用火山引擎豆包系列模型。\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"volcengine\",\n \"llmConfig\": {\n \"apiKey\": \"your-api-key\",\n \"baseURL\": \"https://ark.cn-beijing.volces.com/api/v3\",\n \"model\": \"doubao-seed-1-6-251015\",\n \"embeddingModel\": \"doubao-embedding-text-240715\"\n }\n }\n}\n```\n\n#### 文本生成模型\n\n| 模型 | 上下文 | 费用 (输入/输出 ¥/百万tokens) | 特点 |\n| ------------------------ | ------ | ----------------------------- | ------------------------ |\n| doubao-seed-1.6 | 256K | ¥0.8 / ¥2 | 主力模型,深度思考,推荐 |\n| doubao-seed-1.6-thinking | 256K | ¥1 / ¥2.5 | 强制思考模式 |\n| doubao-seed-1.6-flash | 256K | ¥0.4 / ¥1 | 快速响应,低延迟 |\n| doubao-seed-1.6-lite | 256K | ¥0.3 / ¥0.8 | 最低成本 |\n| doubao-pro-32k | 32K | ¥0.8 / ¥2 | 专业版 |\n| doubao-seed-code | 128K | ¥0.6 / ¥1.8 | 代码生成专用 |\n\n#### 视觉模型\n\n| 模型 | 上下文 | 费用 | 特点 |\n| ---------------------- | ------ | -------------------- | ----------------------------- |\n| doubao-seed-1.6-vision | 256K | ¥2.6/¥7.8/百万tokens | 视觉深度思考,GUI Agent,推荐 |\n| doubao-1.5-vision-pro | 128K | ¥2/¥6/百万tokens | 专业视觉理解 |\n| doubao-1.5-vision-lite | 64K | ¥1/¥3/百万tokens | 轻量视觉 |\n| doubao-1.5-ui-tars | - | ¥2/¥6/百万tokens | GUI自动化专用 |\n\n#### 图像/视频生成模型\n\n| 模型 | 费用 | 特点 |\n| ----------------------- | ------------- | ------------------------------ |\n| doubao-seedream-4.5 | ¥0.08-0.15/张 | 最新图像生成,推荐 |\n| doubao-seedream-4.0 | ¥0.06-0.12/张 | 上一代图像生成 |\n| doubao-seededit-3.0 | ¥0.05/张 | 图像编辑(背景移除、光线调整) |\n| doubao-seedance-1.5-pro | ¥0.3/秒 | 专业视频生成,推荐 |\n| doubao-pixeldance | ¥0.3/秒 | 高动态视频生成 |\n\n#### Embedding模型\n\n| 模型 | 维度 | 费用 | 特点 |\n| ----------------------- | ---- | ---------------- | ---------------- |\n| doubao-embedding-large | 2048 | ¥0.2/百万tokens | 高精度检索,推荐 |\n| doubao-embedding | 1024 | ¥0.15/百万tokens | 标准,中英文 |\n| doubao-embedding-vision | 1536 | ¥0.3/百万tokens | 图像向量化 |\n\n#### 专用模型\n\n| 模型 | 费用 | 特点 |\n| ----------------------- | -------------------- | ---------- |\n| doubao-seed-translation | ¥0.5/¥1.5/百万tokens | 翻译专用 |\n| doubao-seed3d-1.0 | ¥1.5/个 | 3D模型生成 |\n\n---\n\n### DeepSeek\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"deepseek\",\n \"llmConfig\": {\n \"apiKey\": \"sk-...\",\n \"baseURL\": \"https://api.deepseek.com/v1\",\n \"model\": \"deepseek-chat\"\n }\n }\n}\n```\n\n#### 支持的模型\n\n| 模型 | 费用 (输入/输出) | 上下文 | 特点 |\n| ----------------- | ------------------------- | ------ | -------------------- |\n| deepseek-chat | $0.14 / $0.28 / 1M tokens | 128K | 通用对话,性价比极高 |\n| deepseek-coder | $0.14 / $0.28 / 1M tokens | 128K | 代码生成专用 |\n| deepseek-reasoner | $0.55 / $2.19 / 1M tokens | 128K | 深度推理(R1) |\n\n> DeepSeek 支持 Function Calling,桌面端和后端均已集成。\n\n---\n\n### OpenAI\n\n#### 注册和获取API Key\n\n```\n1. 访问 https://platform.openai.com\n2. 注册账号\n3. API Keys → Create new secret key\n4. 复制保存API Key (sk-...)\n```\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"openai\",\n \"llmConfig\": {\n \"apiKey\": \"sk-...\",\n \"model\": \"gpt-4o\",\n \"baseURL\": \"https://api.openai.com/v1\",\n \"temperature\": 0.7,\n \"maxTokens\": 2000\n }\n }\n}\n```\n\n#### 支持的模型\n\n| 模型 | 费用 (输入/输出) | 上下文 | 特点 |\n| ----------- | ------------------------- | ------ | ---------------- |\n| gpt-4o | $2.50 / $10 / 1M tokens | 128K | 最新旗舰,多模态 |\n| gpt-4o-mini | $0.15 / $0.60 / 1M tokens | 128K | 快速便宜 |\n| o1 | $15 / $60 / 1M tokens | 200K | 推理模型 |\n| o3-mini | $1.10 / $4.40 / 1M tokens | 200K | 推理模型(轻量) |\n| gpt-4-turbo | $10 / $30 / 1M tokens | 128K | 长上下文 |\n\n---\n\n### Anthropic Claude\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"anthropic\",\n \"llmConfig\": {\n \"apiKey\": \"sk-ant-...\",\n \"model\": \"claude-sonnet-4-5-20250929\",\n \"temperature\": 0.7,\n \"maxTokens\": 4096\n }\n }\n}\n```\n\n#### 模型对比\n\n| 模型 | 费用 | 上下文 | 特点 |\n| -------------------------- | --------------- | ------ | ----------------------------------------------------------------------- |\n| claude-opus-4-6 | $5 / $25 / 1M | 200K | 最强推理和编码(Opus 4.5+ 已降价;$15/$75 仅适用已退役的 Opus 4 / 4.1) |\n| claude-sonnet-4-5-20250929 | $3 / $15 / 1M | 200K | 平衡性能 |\n| claude-haiku-4-5-20251001 | $0.80 / $4 / 1M | 200K | 快速便宜 |\n\n---\n\n### 国内大模型\n\n#### 通义千问 (DashScope)\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"qwen\",\n \"llmConfig\": {\n \"apiKey\": \"sk-...\",\n \"model\": \"qwen-max\",\n \"baseURL\": \"https://dashscope.aliyuncs.com/api/v1\"\n }\n }\n}\n```\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=dashscope\nDASHSCOPE_API_KEY=sk-...\nDASHSCOPE_MODEL=qwen-turbo\n```\n\n#### 智谱AI (ChatGLM)\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"zhipu\",\n \"llmConfig\": {\n \"apiKey\": \"your-zhipu-key\",\n \"model\": \"glm-4\"\n }\n }\n}\n```\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=zhipu\nZHIPU_API_KEY=your-key\nZHIPU_MODEL=glm-4\n```\n\n> 智谱AI 支持 Function Calling。\n\n#### 百度千帆 (文心一言)\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"ernie\",\n \"llmConfig\": {\n \"apiKey\": \"your_key:secret\",\n \"model\": \"ERNIE-Bot-4\"\n }\n }\n}\n```\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=qianfan\nQIANFAN_API_KEY=ak:sk\nQIANFAN_MODEL=ERNIE-Bot-turbo\n```\n\n#### 腾讯混元\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=hunyuan\nHUNYUAN_API_KEY=your-key\nHUNYUAN_MODEL=hunyuan-lite\n```\n\n> 使用 OpenAI 兼容接口 (`https://api.hunyuan.cloud.tencent.com/v1`)。\n\n#### 讯飞星火\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=spark\nSPARK_API_KEY=app_id:api_key:api_secret\nSPARK_MODEL=spark-lite\n```\n\n> 使用 OpenAI 兼容接口 (`https://spark-api-open.xf-yun.com/v1`)。\n\n#### MiniMax\n\n后端环境变量:\n\n```bash\nLLM_PROVIDER=minimax\nMINIMAX_API_KEY=your-key\nMINIMAX_MODEL=abab5.5-chat\n```\n\n> 使用 OpenAI 兼容接口 (`https://api.minimax.chat/v1`)。\n\n#### Moonshot/Kimi\n\n通过桌面端 Custom 提供商或 OpenAI 兼容接口接入:\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"custom\",\n \"llmConfig\": {\n \"apiKey\": \"your-moonshot-key\",\n \"baseURL\": \"https://api.moonshot.cn/v1\",\n \"model\": \"moonshot-v1-8k\",\n \"name\": \"Moonshot\"\n }\n }\n}\n```\n\n支持模型:`moonshot-v1-8k`, `moonshot-v1-32k`, `moonshot-v1-128k`(长上下文)\n\n#### 硅基流动 (SiliconFlow)\n\n聚合多种开源模型,价格极低:\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"custom\",\n \"llmConfig\": {\n \"apiKey\": \"your-siliconflow-key\",\n \"baseURL\": \"https://api.siliconflow.cn/v1\",\n \"model\": \"Qwen/Qwen2.5-72B-Instruct\",\n \"name\": \"SiliconFlow\"\n }\n }\n}\n```\n\n支持模型:`Qwen2-7B`, `Qwen2.5-72B`, `DeepSeek-V2.5` 等,费用低至 ¥0.7/百万tokens\n\n#### 零一万物 (Yi)\n\n```json\n{\n \"ai\": {\n \"llmProvider\": \"custom\",\n \"llmConfig\": {\n \"apiKey\": \"your-yi-key\",\n \"baseURL\": \"https://api.lingyiwanwu.com/v1\",\n \"model\": \"yi-large\",\n \"name\": \"Yi\"\n }\n }\n}\n```\n\n---\n\n## 多模态视觉模型\n\n### LLaVA 本地视觉模型\n\nChainlessChain 内置 LLaVA 客户端,支持本地多模态视觉理解:\n\n```bash\n# 安装LLaVA模型\nollama pull llava:7b\n```\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"visionModel\": \"llava:7b\",\n \"visionConfig\": {\n \"baseURL\": \"http://localhost:11434\",\n \"timeout\": 300000,\n \"maxImageSize\": 5242880\n }\n }\n}\n```\n\n支持的图片格式:`.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.bmp`\n\n### 云端视觉模型\n\n| 提供商 | 模型 | 特点 |\n| --------- | ---------------------- | ----------------------- |\n| 火山引擎 | doubao-seed-1.6-vision | 视觉深度思考,GUI Agent |\n| OpenAI | gpt-4o | 图像+视频理解 |\n| Anthropic | claude-sonnet-4-5 | 图像理解 |\n\n> Computer Use 功能集成了 Vision AI,支持自动截图+视觉分析实现GUI自动化。详见 [Computer Use →](/chainlesschain/computer-use)\n\n---\n\n## 智能模型路由\n\n### Multi-Model Router 技能\n\nChainlessChain 内置智能模型路由技能 (`/multi-model-router`),根据任务复杂度自动选择最优模型:\n\n```\n/multi-model-router --route fix a typo in README.md\n→ Complexity: 1/10. Recommended: haiku (fastest, cheapest).\n\n/multi-model-router --route refactor the entire authentication system across 15 files\n→ Complexity: 9/10. Recommended: opus (strongest reasoning).\n```\n\n#### 模型能力矩阵\n\n| 模型 | 推理 | 编码 | 速度 | 成本 | 适用场景 |\n| --------------- | ---- | ---- | ---- | ---- | ------------------- |\n| opus | 10 | 9 | 3 | 10 | 架构设计、复杂调试 |\n| sonnet | 8 | 9 | 7 | 5 | 代码生成、重构 |\n| haiku | 5 | 6 | 10 | 1 | 简单编辑、格式化 |\n| gpt-4o | 9 | 8 | 6 | 7 | 通用任务 |\n| gpt-4o-mini | 6 | 6 | 9 | 2 | 快速问答、翻译 |\n| qwen2 | 7 | 7 | 8 | 0 | 本地推理(免费) |\n| doubao-seed-1.6 | 8 | 8 | 7 | 2 | 通用+深度思考 |\n| deepseek-chat | 8 | 9 | 7 | 1 | 代码+推理(极低成本) |\n\n### 火山引擎智能模型选择器\n\n桌面端内置 VolcEngine Model Selector,支持按任务类型自动选择豆包模型:\n\n```javascript\n// 任务类型 → 自动选择最优模型\nconst taskTypes = {\n chat: \"doubao-seed-1.6\", // 通用对话\n long_context: \"doubao-seed-1.6\", // 长文本处理\n complex_reasoning: \"doubao-seed-1.6-thinking\", // 复杂推理\n image_understanding: \"doubao-seed-1.6-vision\", // 图像理解\n code_writing: \"doubao-seed-code\", // 代码编写\n fast_response: \"doubao-seed-1.6-flash\", // 快速响应\n cost_effective: \"doubao-seed-1.6-lite\", // 成本优化\n gui_automation: \"doubao-1.5-ui-tars\", // GUI自动化\n translation: \"doubao-seed-translation\", // 翻译\n};\n```\n\n### 自动回退机制\n\n后端 AI Service 具备智能 Fallback 机制:\n\n- 当配置的云LLM API Key 未设置或为空时,自动回退到 Ollama 本地模型\n- 确保服务始终可用,不因API配置缺失而中断\n\n---\n\n## Context Engineering (KV-Cache优化)\n\nChainlessChain 集成 Manus 风格的 Context Engineering 优化:\n\n### 核心优化\n\n- **静态/动态分离**: 将静态系统提示词放在对话开头,提高 KV-Cache 命中率 (60-85%)\n- **工具定义序列化**: 按名称确定性排序,避免缓存失效\n- **任务目标重述**: 在上下文中间重述目标,防止\"迷失在中间\"\n- **错误历史追踪**: 从过去的错误中学习\n- **可恢复压缩**: 保留引用以便后续恢复\n- **Token估算**: 中英文自动检测\n\n### 配置\n\n```json\n{\n \"ai\": {\n \"contextEngineering\": {\n \"enabled\": true,\n \"kvCacheOptimization\": true,\n \"toolMasking\": true,\n \"taskTracking\": true,\n \"maxContextTokens\": 128000\n }\n }\n}\n```\n\n> 详见 Context Engineering IPC (17个处理器) 的完整配置。\n\n---\n\n## Embedding模型配置\n\n### 本地Embedding\n\n#### 使用sentence-transformers\n\n```python\nfrom sentence_transformers import SentenceTransformer\n\n# PC端 - 大模型\nmodel = SentenceTransformer('BAAI/bge-large-zh-v1.5')\n\n# 移动端 - 小模型\nmodel_mobile = SentenceTransformer('BAAI/bge-small-zh-v1.5')\n\n# 向量化\nembeddings = model.encode(['文本1', '文本2'])\n```\n\n#### 配置\n\n```json\n{\n \"ai\": {\n \"embeddingProvider\": \"local\",\n \"embeddingConfig\": {\n \"model\": \"BAAI/bge-large-zh-v1.5\",\n \"dimensions\": 1024,\n \"maxBatchSize\": 32,\n \"device\": \"cuda\"\n }\n }\n}\n```\n\n### 云端Embedding\n\n#### OpenAI Embeddings\n\n```json\n{\n \"ai\": {\n \"embeddingProvider\": \"openai\",\n \"embeddingConfig\": {\n \"apiKey\": \"sk-...\",\n \"model\": \"text-embedding-3-small\",\n \"dimensions\": 1536\n }\n }\n}\n```\n\n费用: $0.02 / 1M tokens (text-embedding-3-small)\n\n#### 火山引擎 Embedding\n\n```json\n{\n \"ai\": {\n \"embeddingProvider\": \"volcengine\",\n \"embeddingConfig\": {\n \"apiKey\": \"your-api-key\",\n \"model\": \"doubao-embedding-text-240715\",\n \"dimensions\": 1024\n }\n }\n}\n```\n\n费用: ¥0.15 / 百万tokens\n\n---\n\n## RAG配置\n\n### 基本配置\n\n```json\n{\n \"ai\": {\n \"ragEnabled\": true,\n \"ragConfig\": {\n \"topK\": 5,\n \"minScore\": 0.7,\n \"reranking\": true,\n \"contextWindow\": 4000,\n \"chunkSize\": 500,\n \"chunkOverlap\": 50\n }\n }\n}\n```\n\n### 高级配置\n\n#### 混合检索\n\nChainlessChain 内置 Hybrid Search Engine,支持 Vector + BM25 双路检索 + RRF融合:\n\n```json\n{\n \"ragConfig\": {\n \"retrievalMode\": \"hybrid\",\n \"vectorWeight\": 0.6,\n \"keywordWeight\": 0.4,\n \"fusion\": \"rrf\"\n }\n}\n```\n\n- **Vector Search**: 语义相似度,通过 RAG Manager Embedding\n- **BM25 Search**: Okapi BM25 算法,中英文分词器\n- **RRF Fusion**: Reciprocal Rank Fusion 结果合并\n- **性能**: <20ms 搜索延迟,并行执行\n\n#### 重排序\n\n```json\n{\n \"ragConfig\": {\n \"reranking\": true,\n \"rerankerModel\": \"BAAI/bge-reranker-large\",\n \"rerankerTopK\": 3\n }\n}\n```\n\n---\n\n## 查询模板\n\n### 创建模板\n\n```\n设置 → AI → 查询模板 → 新建\n\n模板信息:\n- 名称: 编程助手\n- 关联知识库: 技术文档、代码笔记\n- LLM模型: qwen2:7b\n- RAG模型: bge-large-zh-v1.5\n- 系统提示词: (见下方)\n- Temperature: 0.3\n- Max Tokens: 3000\n```\n\n### 系统提示词示例\n\n#### 通用问答\n\n```\n你是一个智能助手,基于用户的个人知识库回答问题。\n请遵循以下原则:\n1. 只使用检索到的知识库内容回答\n2. 如果知识库中没有相关信息,明确告知用户\n3. 提供详细准确的答案\n4. 必要时引用原文\n```\n\n#### 编程助手\n\n```\n你是一个专业的编程助手,帮助用户解决编程问题。\n知识库包含用户的技术笔记和代码片段。\n\n回答要求:\n1. 基于知识库中的代码示例\n2. 提供完整可运行的代码\n3. 解释代码逻辑\n4. 指出潜在问题\n5. 建议最佳实践\n```\n\n#### 文案优化\n\n```\n你是一个专业的文案编辑,帮助用户优化文字表达。\n\n任务:\n1. 分析用户的草稿\n2. 提出改进建议\n3. 提供优化后的版本\n4. 保持原意,提升表达质量\n```\n\n---\n\n## LLM性能监控\n\nChainlessChain 内置 LLM Performance Dashboard(路由: `#/llm/performance`),提供全面的模型使用分析:\n\n### 监控指标\n\n- **Token 使用量**: 输入/输出 Token 统计,每日趋势\n- **成本分析**: 各提供商费用追踪,成本预测\n- **响应延迟**: 首 Token 时间、完整响应时间分布\n- **缓存命中率**: Embedding 缓存、Response 缓存效果\n- **模型对比**: 不同模型的性能/成本对比\n\n### 配置\n\n```json\n{\n \"ai\": {\n \"tokenTracking\": {\n \"enabled\": true,\n \"budgetAlert\": {\n \"daily\": 100000,\n \"monthly\": 2000000\n }\n }\n }\n}\n```\n\n> 详见 [LLM Performance Dashboard 文档](https://github.com/chainlesschain/chainlesschain/blob/main/docs/features/LLM_PERFORMANCE_DASHBOARD.md)\n\n---\n\n## 性能优化\n\n### 缓存策略\n\n```json\n{\n \"ai\": {\n \"cacheEnabled\": true,\n \"cacheConfig\": {\n \"embeddingCache\": true,\n \"embeddingCacheTTL\": 86400,\n \"llmResponseCache\": false,\n \"maxCacheSize\": 1000\n }\n }\n}\n```\n\nEmbedding 缓存使用 SQLite 存储,避免重复计算向量。\n\n### 批处理\n\n```python\n# 批量向量化\ntexts = ['文本1', '文本2', ..., '文本100']\n\n# 不推荐:逐个处理\nembeddings = [model.encode(text) for text in texts]\n\n# 推荐:批处理\nembeddings = model.encode(texts, batch_size=32)\n```\n\n### GPU加速\n\n```json\n{\n \"ai\": {\n \"device\": \"cuda\",\n \"gpuMemoryFraction\": 0.8,\n \"precision\": \"fp16\"\n }\n}\n```\n\n---\n\n## 成本优化\n\n### 混合部署策略\n\n```typescript\nclass AIRouter {\n async route(query: string, context: any) {\n // 1. 简单问题 → 本地小模型\n if (context.length < 1000) {\n return this.localLLM.generate(query);\n }\n\n // 2. 复杂问题 → 本地大模型\n if (this.localLargeModel.available && !this.needHighQuality(query)) {\n return this.localLargeModel.generate(query);\n }\n\n // 3. 高质量要求 → 云端API\n return this.cloudAPI.generate(query);\n }\n}\n```\n\n### Token使用优化\n\n```typescript\n// 1. 精简提示词\nconst systemPrompt = \"你是助手,简洁回答。\"; // 而非长篇大论\n\n// 2. 限制上下文\nconst context = relevantDocs.slice(0, 3); // 只用最相关的3个文档\n\n// 3. 控制输出长度\nconst maxTokens = 500; // 根据需求调整\n\n// 4. 使用Context Engineering优化KV-Cache命中率\n// 静态内容前置,动态内容后置\n```\n\n### 成本对比\n\n| 方案 | 月成本估算 | 适用场景 |\n| ---------------------- | ---------- | --------------------- |\n| 纯本地 (Ollama) | ¥0 (电费) | 隐私优先、离线场景 |\n| 豆包 Seed 1.6 Lite | ¥5-50 | 日常对话、性价比首选 |\n| DeepSeek Chat | ¥5-30 | 代码+推理、极致性价比 |\n| GPT-4o-mini | ¥10-100 | 通用任务 |\n| GPT-4o / Claude Sonnet | ¥50-500 | 专业开发 |\n| GPT-o1 / Claude Opus | ¥200-2000 | 复杂推理 |\n\n---\n\n## 模型评估\n\n### 质量评估\n\n```python\n# 评估检索质量\ndef evaluate_retrieval(test_queries, ground_truth):\n scores = {\n 'precision': [],\n 'recall': [],\n 'ndcg': []\n }\n\n for query, truth in zip(test_queries, ground_truth):\n retrieved = rag.retrieve(query)\n scores['precision'].append(precision(retrieved, truth))\n scores['recall'].append(recall(retrieved, truth))\n scores['ndcg'].append(ndcg(retrieved, truth))\n\n return scores\n\n# 评估生成质量\ndef evaluate_generation(test_cases):\n scores = []\n for query, expected_answer in test_cases:\n generated = llm.generate(query)\n score = similarity(generated, expected_answer)\n scores.append(score)\n return np.mean(scores)\n```\n\n### 性能监控\n\n```\n设置 → AI → 性能监控\n\n指标:\n- 平均响应时间\n- 每日Token消耗\n- 检索精度\n- 用户满意度(评分)\n- KV-Cache命中率\n\n图表:\n- 每日使用量趋势\n- 不同模型的成本对比\n- 响应速度分布\n- 缓存命中率趋势\n```\n\n---\n\n## 使用示例\n\n### Ollama 本地模型配置与使用\n\n```bash\n# 安装并启动 Ollama\nollama serve\n\n# 下载推荐模型\nollama pull qwen2:7b\n\n# 通过 CLI 测试模型连通性\nchainlesschain llm test\nchainlesschain llm models # 列出已安装模型\n```\n\n### 切换 LLM 提供商\n\n```bash\n# 切换到 DeepSeek(高性价比云端)\nchainlesschain llm switch deepseek\nchainlesschain config set llm.apiKey sk-xxx\n\n# 切换到 Ollama 本地模型\nchainlesschain llm switch ollama\n\n# 查看当前提供商状态\nchainlesschain llm providers\n```\n\n### 在对话中使用不同模型\n\n```bash\n# 单次问答(使用当前配置的模型)\nchainlesschain ask \"解释量子计算的基本原理\"\n\n# 交互式对话(支持流式输出)\nchainlesschain chat\n\n# Agent 模式(146 技能 + Plan Mode)\nchainlesschain agent\n```\n\n---\n\n## 故障排查\n\n### 本地模型加载失败\n\n**常见问题**:\n\n```\nError: Failed to load model\n\n原因:\n1. 内存不足\n2. 模型文件损坏\n3. 依赖缺失\n\n解决:\n1. 检查内存: htop / 任务管理器\n2. 重新下载模型: ollama pull qwen2:7b\n3. 检查日志: ~/.ollama/logs/\n```\n\n### API调用失败\n\n**检查清单**:\n\n```\n□ API Key是否正确\n□ 账户余额是否充足\n□ 网络连接是否正常\n□ 是否超过速率限制\n□ baseURL是否正确\n□ 提供商服务是否正常\n```\n\n### 智能Fallback失效\n\n如果云端API失败但没有自动回退到Ollama:\n\n```\n检查:\n1. Ollama服务是否运行: curl http://localhost:11434/api/tags\n2. 本地模型是否已下载: ollama list\n3. 配置中fallback是否启用\n```\n\n### 响应速度慢\n\n**优化步骤**:\n\n```\n1. 使用更快的模型(doubao-seed-1.6-flash、Phi-3、Gemma)\n2. 减少上下文长度\n3. 启用GPU加速\n4. 使用量化模型(Q4, Q8)\n5. 启用Context Engineering的KV-Cache优化\n6. 考虑使用云端API\n```\n\n---\n\n## 常见问题\n\n### 本地模型vs云端API,如何选择?\n\n```\n本地模型适合:\n✓ 隐私敏感数据\n✓ 离线使用场景\n✓ 长期大量使用(成本低)\n✓ 实时响应要求\n\n云端API适合:\n✓ 对质量要求极高\n✓ 使用量不大\n✓ 无本地算力\n✓ 需要最新功能\n```\n\n### 如何降低成本?\n\n```\n策略:\n1. 本地模型为主,云端为辅\n2. 使用Multi-Model Router按任务复杂度路由\n3. 缓存常见问题的答案(Embedding Cache + Response Cache)\n4. 优化提示词,减少Token消耗\n5. 使用Context Engineering提升KV-Cache命中率\n6. 使用更便宜的模型(DeepSeek、豆包Lite、GPT-4o-mini、Haiku)\n7. 批处理请求\n```\n\n### 模型推荐?\n\n```\n个人使用:\n- PC: Qwen2:7B (本地) + 豆包Seed 1.6 Lite (云端,最低成本)\n- 移动: MiniCPM-2B (本地)\n- 云端首选: DeepSeek Chat (性价比之王)\n\n专业开发者:\n- PC: LLaMA3:70B (本地) + Claude Sonnet 4.5 (云端)\n- 代码: DeepSeek Coder + doubao-seed-code\n- 推理: DeepSeek Reasoner / Claude Opus 4.6\n\n企业:\n- 自建: LLaMA3:70B 集群\n- 国内云端: 豆包Seed 1.6 (默认) / 通义千问Max\n- 国际云端: GPT-4o / Claude Sonnet 4.5\n- 合规审计: 配合Enterprise Audit系统\n```\n\n### 为什么默认使用火山引擎豆包?\n\n```\n原因:\n1. 国内访问稳定,无需代理\n2. 256K超长上下文窗口\n3. 价格极具竞争力(¥0.3-2/百万tokens)\n4. 支持深度思考模式\n5. 完整多模态能力(视觉/图像/视频/3D)\n6. 原生Function Calling支持\n7. 中文能力优秀\n```\n\n---\n\n## 已实现功能\n\n- [x] 14+ 云LLM提供商集成(Ollama、OpenAI、Anthropic、DeepSeek、火山引擎、通义千问、智谱AI、百度千帆、腾讯混元、讯飞星火、MiniMax等)\n- [x] 多模态视觉模型(LLaVA本地 + 豆包Vision + GPT-4V + Claude Vision)\n- [x] 智能模型路由(Multi-Model Router技能)\n- [x] Context Engineering KV-Cache优化\n- [x] LLM Performance Dashboard(Token追踪、成本分析)\n- [x] 混合检索引擎(Vector + BM25 + RRF融合)\n- [x] Embedding缓存(SQLite存储)\n- [x] 响应缓存和流式输出\n- [x] 火山引擎智能模型选择器(按任务类型自动选择)\n- [x] 图像生成(豆包Seedream 3.0/4.0/4.5)\n- [x] 视频生成(豆包Seedance、PixelDance)\n- [x] 图像编辑(豆包SeedEdit)\n- [x] 3D模型生成(doubao-seed3d-1.0)\n- [x] GUI自动化模型(doubao-1.5-ui-tars)\n- [x] 翻译专用模型(doubao-seed-translation)\n- [x] 智能Fallback(云端失败自动回退Ollama)\n- [x] API Key加密存储(AES-256-GCM)\n\n## 未来规划\n\n- [x] 模型微调支持(LoRA/QLoRA本地微调)\n- [x] 语音模型集成(Whisper语音识别 + TTS合成)\n- [x] 联邦学习(分布式模型训练,保护隐私)\n- [x] 更多模型支持(Google Gemini、Mistral、零一万物Yi等)\n- [x] 本地模型量化工具(一键GGUF/GPTQ量化)\n- [x] 模型性能基准测试(自动化评估框架)\n- [x] 多模型协作(Architect+Editor双模型模式)\n- [x] 长期记忆增强(结合Permanent Memory的个性化模型)\n\n## 配置参考\n\n### 完整 LLM 提供商配置结构\n\n```js\n// desktop-app-vue/src/main/config/unified-config-manager.js\n// .chainlesschain/config.json — ai 段完整字段\n{\n \"ai\": {\n // 主提供商选择: \"ollama\" | \"volcengine\" | \"openai\" | \"anthropic\" |\n // \"deepseek\" | \"qwen\" | \"zhipu\" | \"ernie\" | \"custom\"\n \"llmProvider\": \"volcengine\",\n\n // 提供商连接参数 (与 llmProvider 对应)\n \"llmConfig\": {\n \"apiKey\": \"your-api-key\", // 云端提供商 API Key(AES-256-GCM 加密存储)\n \"baseURL\": \"https://ark.cn-beijing.volces.com/api/v3\", // 自定义端点\n \"model\": \"doubao-seed-1.6-251015\", // 文本生成主模型\n \"embeddingModel\": \"doubao-embedding-text-240715\", // Embedding 模型\n \"temperature\": 0.7, // 采样温度 (0.0–1.0)\n \"maxTokens\": 4096, // 单次最大输出 token\n \"contextWindow\": 131072 // 上下文窗口大小\n },\n\n // 视觉模型单独配置\n \"visionModel\": \"doubao-seed-1.6-vision\",\n \"visionConfig\": {\n \"baseURL\": \"http://localhost:11434\", // 本地 LLaVA 时填 Ollama 地址\n \"timeout\": 300000,\n \"maxImageSize\": 5242880 // 最大图片尺寸 (bytes)\n },\n\n // Embedding 提供商: \"local\" | \"openai\" | \"volcengine\"\n \"embeddingProvider\": \"volcengine\",\n \"embeddingConfig\": {\n \"apiKey\": \"your-api-key\",\n \"model\": \"doubao-embedding-text-240715\",\n \"dimensions\": 1024,\n \"maxBatchSize\": 32\n },\n\n // Context Engineering (KV-Cache 优化)\n \"contextEngineering\": {\n \"enabled\": true,\n \"kvCacheOptimization\": true, // 静态/动态内容分离\n \"toolMasking\": true, // 按激活技能过滤工具集\n \"taskTracking\": true, // 任务目标重述\n \"maxContextTokens\": 128000\n },\n\n // RAG 混合检索\n \"ragEnabled\": true,\n \"ragConfig\": {\n \"retrievalMode\": \"hybrid\", // \"vector\" | \"bm25\" | \"hybrid\"\n \"topK\": 5,\n \"minScore\": 0.7,\n \"vectorWeight\": 0.6,\n \"keywordWeight\": 0.4,\n \"fusion\": \"rrf\", // Reciprocal Rank Fusion\n \"reranking\": true,\n \"rerankerModel\": \"BAAI/bge-reranker-large\",\n \"contextWindow\": 4000,\n \"chunkSize\": 500,\n \"chunkOverlap\": 50\n },\n\n // Token 用量追踪与预算\n \"tokenTracking\": {\n \"enabled\": true,\n \"budgetAlert\": {\n \"daily\": 100000, // 日预算 token 上限\n \"monthly\": 2000000 // 月预算 token 上限\n }\n },\n\n // Embedding 响应缓存\n \"cacheEnabled\": true,\n \"cacheConfig\": {\n \"embeddingCache\": true,\n \"embeddingCacheTTL\": 86400, // 秒,默认 24h\n \"llmResponseCache\": false,\n \"maxCacheSize\": 1000\n },\n\n // GPU 加速 (Embedding / 本地推理)\n \"device\": \"cuda\", // \"cpu\" | \"cuda\" | \"mps\"\n \"gpuMemoryFraction\": 0.8,\n \"precision\": \"fp16\"\n }\n}\n```\n\n### 模型智能路由配置\n\n```js\n// llm-manager.js — LLM_CATEGORIES 与 Category Routing\n// 用于 resolveCategory(category, { skill }) 方法\n\n// 可选类别值\nconst LLM_CATEGORIES = {\n QUICK: \"quick\", // 补全/简单改写 → 优先本地 ollama\n DEEP: \"deep\", // 长上下文/架构 → 优先 anthropic / openai\n REASONING: \"reasoning\", // 推理密集 → 优先 deepseek / o1\n VISION: \"vision\", // 多模态 → 优先 gemini / gpt-4o\n CREATIVE: \"creative\", // 文案/UI创作 → 优先 anthropic\n EMBEDDING: \"embedding\", // 文本向量化 → 优先 volcengine / openai\n AUDIO: \"audio\", // 语音识别/TTS → 优先 openai whisper\n ASR: \"asr\", // 语音转文字 → 优先 openai / gemini\n AUDIO_ANALYSIS: \"audio-analysis\", // 节拍检测/音频特征 → 优先本地 ollama\n VIDEO_VLM: \"video-vlm\", // 视频理解 VLM → 优先 gemini / openai\n};\n\n// SKILL.md 中声明 model-hints 触发自动类别推断\n// capability: \"reasoning\" → REASONING\n// capability: \"transcription\" → ASR\n// capability: \"beat-detection\" → AUDIO_ANALYSIS\n// capability: \"video-review\" → VIDEO_VLM\n// context-window: \"large\" → DEEP\n```\n\n### 自定义 OpenAI 兼容端点\n\n```js\n// 适用于 Moonshot、SiliconFlow、零一万物、本地 vLLM 等 OpenAI 兼容服务\n{\n \"ai\": {\n \"llmProvider\": \"custom\",\n \"llmConfig\": {\n \"apiKey\": \"your-provider-key\",\n \"baseURL\": \"https://api.siliconflow.cn/v1\", // 替换为实际端点\n \"model\": \"Qwen/Qwen2.5-72B-Instruct\",\n \"name\": \"SiliconFlow\", // 显示名称\n \"temperature\": 0.7,\n \"maxTokens\": 4096\n }\n }\n}\n```\n\n---\n\n## 性能指标\n\n### 推理延迟(首 Token 时间,TTFT)\n\n| 模型 | 硬件条件 | 平均 TTFT | P95 TTFT | 备注 |\n| ------------------------ | ------------- | --------- | -------- | ---------------- |\n| Phi-3 Mini 3.8B (Q4) | CPU 16GB RAM | 320 ms | 680 ms | 低配 PC 推荐 |\n| Qwen2 7B (Q4) | CPU 16GB RAM | 580 ms | 1200 ms | 通用本地推荐 |\n| Qwen2 7B (Q4) | GPU RTX 3060 | 95 ms | 180 ms | GPU 加速 |\n| LLaMA3 8B (Q4) | CPU 16GB RAM | 620 ms | 1300 ms | - |\n| LLaMA3 8B (Q4) | GPU RTX 3060 | 110 ms | 210 ms | GPU 加速 |\n| LLaMA3 70B (Q4) | GPU A100 40GB | 280 ms | 520 ms | 专业服务器 |\n| doubao-seed-1.6-flash | 云端 API | 180 ms | 420 ms | 低延迟模型 |\n| doubao-seed-1.6 | 云端 API | 350 ms | 780 ms | 标准主力模型 |\n| doubao-seed-1.6-thinking | 云端 API | 1200 ms | 3500 ms | 深度思考模式 |\n| deepseek-chat | 云端 API | 280 ms | 650 ms | 高性价比 |\n| gpt-4o | 云端 API | 420 ms | 950 ms | OpenAI 旗舰 |\n| gpt-4o-mini | 云端 API | 190 ms | 420 ms | 快速便宜 |\n| claude-sonnet-4-5 | 云端 API | 380 ms | 820 ms | Anthropic 平衡版 |\n\n### Token 吞吐量(输出速率)\n\n| 模型 | 硬件条件 | 平均吞吐 (tokens/s) | 备注 |\n| --------------------- | ------------- | ------------------- | ------------ |\n| Phi-3 Mini 3.8B (Q4) | CPU 16GB RAM | 12–18 t/s | 低配 PC |\n| Qwen2 7B (Q4) | CPU 16GB RAM | 8–14 t/s | 通用本地 |\n| Qwen2 7B (Q4) | GPU RTX 3060 | 45–65 t/s | GPU 加速 |\n| LLaMA3 8B (Q4) | GPU RTX 3060 | 50–70 t/s | GPU 加速 |\n| LLaMA3 70B (Q4) | GPU A100 40GB | 28–40 t/s | 专业服务器 |\n| doubao-seed-1.6-flash | 云端 API | 80–120 t/s | 低延迟模型 |\n| doubao-seed-1.6 | 云端 API | 45–80 t/s | 标准主力模型 |\n| deepseek-chat | 云端 API | 50–90 t/s | 高性价比 |\n| gpt-4o-mini | 云端 API | 70–110 t/s | 快速便宜 |\n| gpt-4o | 云端 API | 40–65 t/s | OpenAI 旗舰 |\n\n### Context Engineering KV-Cache 命中率\n\n| 场景 | 命中率 | Token 节省 | 说明 |\n| -------------------------- | ------ | ---------- | ------------------------- |\n| 单轮对话(短提示词) | 25–40% | 15–25% | 系统提示词命中 |\n| 多轮对话(同会话) | 60–75% | 40–55% | 历史上下文命中 |\n| Agent 模式(工具定义稳定) | 70–85% | 50–65% | 工具定义 + 系统提示词命中 |\n| RAG 增强问答 | 55–70% | 35–50% | 文档块重复检索命中 |\n\n### Embedding 与 RAG 性能\n\n| 指标 | 本地 (BGE-large) | 云端 (doubao-embedding) | 云端 (OpenAI ada-3) |\n| -------------------- | ---------------- | ----------------------- | ------------------- |\n| 向量化延迟 (单条) | 8–15 ms (GPU) | 35–60 ms | 40–70 ms |\n| 批处理吞吐 (32条) | 180–280 条/s | 受 API 限速 | 受 API 限速 |\n| 混合检索延迟 | <20 ms | <20 ms | <20 ms |\n| 检索精度 (MRR@5) | 0.81 | 0.87 | 0.85 |\n| Embedding 缓存命中率 | — | 65–80% | 65–80% |\n\n---\n\n## 测试覆盖率\n\n### LLM 核心模块测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| -------------------------------------------------------------------------------- | ------------------------------------------------------- | ------ |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/llm-manager.test.js` | LLMManager 多提供商路由、chat()、fallback | 48 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/llm-manager-category-routing.test.js` | Category Routing (quick/deep/reasoning/vision/creative) | 26 |\n| ✅ `desktop-app-vue/tests/unit/llm/llm-manager-media-categories.test.js` | 媒体类别扩展 (asr/audio-analysis/video-vlm) | 25 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/token-tracker.test.js` | Token 用量统计、成本计算、预算告警 | 22 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/prompt-compressor.test.js` | 去重/截断/总结三层压缩策略 | 18 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/context-engineering.test.js` | KV-Cache 优化、工具过滤、任务追踪 | 31 |\n| ✅ `desktop-app-vue/src/main/llm/__tests__/volcengine-model-selector.test.js` | 火山引擎任务类型→模型自动选择 | 19 |\n\n### RAG 与 Embedding 测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| ---------------------------------------------------------------- | ----------------------------------- | ------ |\n| ✅ `desktop-app-vue/tests/unit/rag/hybrid-search-engine.test.js` | Vector + BM25 双路检索 + RRF 融合 | 34 |\n| ✅ `desktop-app-vue/tests/unit/rag/embedding-cache.test.js` | SQLite Embedding 缓存读写、TTL 过期 | 16 |\n| ✅ `desktop-app-vue/tests/unit/rag/rag-manager.test.js` | RAG 检索、重排序、上下文组装 | 27 |\n\n### 多模态与视觉模型测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| -------------------------------------------------------------------- | -------------------------------- | ------ |\n| ✅ `desktop-app-vue/tests/unit/llm/llava-client.test.js` | LLaVA 本地视觉模型调用、图片编码 | 14 |\n| ✅ `desktop-app-vue/tests/unit/ai-engine/multi-model-router.test.js` | 任务复杂度评分、模型推荐逻辑 | 21 |\n\n### 视频生成集成测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| ------------------------------------------------------------ | ------------------------------------------- | ------ |\n| ✅ `desktop-app-vue/tests/unit/llm/volcengine-video.test.js` | Seedance text-to-video 任务提交/轮询 (mock) | 18 |\n| ✅ `packages/cli/__tests__/video/video-generate.test.js` | CLI video 生成命令端到端 (mock API) | 12 |\n\n### CLI LLM 命令测试\n\n| 测试文件 | 覆盖功能 | 测试数 |\n| ------------------------------------------------------ | --------------------------------------------- | ------ |\n| ✅ `packages/cli/__tests__/unit/llm-providers.test.js` | CLI 提供商切换、providers 列表 | 17 |\n| ✅ `packages/cli/__tests__/unit/llm-switch.test.js` | `chainlesschain llm switch` 持久化配置 | 11 |\n| ✅ `packages/cli/__tests__/unit/stream-router.test.js` | StreamRouter NDJSON 输出、provider/model 透传 | 24 |\n\n---\n\n## 安全考虑\n\n- **API Key 加密存储**: 所有 API Key 使用 AES-256-GCM 加密后存储在本地 `config.json` 中,不以明文保存\n- **本地推理隐私**: 使用 Ollama 本地模型时数据完全离线,不发送到任何外部服务器\n- **传输加密**: 云端 API 调用强制使用 HTTPS/TLS 加密传输\n- **密钥轮换**: 建议定期更换云端 API Key,旧 Key 及时在提供商控制台吊销\n- **预算控制**: 启用 Token 追踪和预算告警(`tokenTracking.budgetAlert`),防止意外超支\n- **敏感数据过滤**: 发送到云端 API 的内容建议经过 DLP 引擎扫描,避免泄露敏感信息\n- **Fallback 安全**: 智能回退到 Ollama 时数据不会离开本地,保障隐私\n- **模型来源**: 仅从官方渠道(Ollama Hub、Hugging Face)下载模型,避免使用不可信来源的模型文件\n\n---\n\n## 关键文件\n\n| 文件 | 职责 |\n| ------------------------------------------------------------------------ | --------------------------------- |\n| `desktop-app-vue/src/main/llm/llm-session.js` | LLM 会话管理与流式输出 |\n| `desktop-app-vue/src/main/llm/llm-providers.js` | 多提供商抽象层 |\n| `desktop-app-vue/src/main/llm/context-engineering.js` | Context Engineering KV-Cache 优化 |\n| `desktop-app-vue/src/main/rag/hybrid-search-engine.js` | Vector + BM25 混合检索 |\n| `desktop-app-vue/src/main/ai-engine/cowork/skills/multi-model-router.js` | 智能模型路由技能 |\n| `desktop-app-vue/src/main/llm/volcengine-model-selector.js` | 火山引擎模型选择器 |\n\n## 相关文档\n\n- [Computer Use 视觉自动化](/chainlesschain/computer-use)\n- [Skills 技能系统](/chainlesschain/skills)\n- [Cowork 多智能体协作](/chainlesschain/cowork)\n- [RAG 检索增强生成](/chainlesschain/knowledge-base)\n"}}, "docs-site/docs/chainlesschain/web-search.md": {"4c95005a30fd890e2e4175c2cceaa3f8d9e2fc05": {"t": 1785674662.848901, "n": 9429, "h": "00d8d6dc522b175b", "c": "# 联网搜索工具(web_search)\n\n> **版本: v0.162+ | 状态: ✅ 生产可用 | 7 个可插拔搜索源 | 工具数 17→18 | 21 单元测试 + 真查实测**\n>\n> `web_search` 是 `cc agent` 的内置工具,补齐 agent 此前「能 `web_fetch` 抓取、却不能搜索」的能力缺口。后端**可插拔**——通过 `.chainlesschain/config.json` 的 `webSearch` 字段在 7 个搜索源之间切换,`auto` 模式按已配置的密钥自动择优,无密钥时回退到免费源。\n\n## 概述\n\nagent 要回答「最新发生了什么」就需要联网搜索。`web_search` 接收查询词,返回归一化的结果列表 `{title, url, snippet}`(部分源附带合成 `answer`),供 agent 进一步推理或配合 `web_fetch` 深读。它的设计镜像 `web_fetch`:网络层 `src/lib/web-search.js` 单点实现,provider/HTTP 失败**不抛异常**而转为 `{error}`,让 agent 自行适配重试或换源。\n\n搜索源是**配置开关**而非写死:你可以只用免费的 DuckDuckGo,也可以配置 Tavily/Brave/博查/千帆等带密钥的源以获得更稳定、更适合国内网络的结果。\n\n## 核心特性\n\n- 🔌 **7 个可插拔搜索源**:4 个带密钥 + 3 个免密钥,经 `webSearch` 配置切换\n- 🤖 **auto 自动择优**:按 `tavily > brave > bocha > qianfan` 顺序选已配置密钥的源,都没有则回退免费 `duckduckgo`\n- 🆓 **免密钥可用**:开箱即用,无需任何密钥即可联网搜索\n- 🌐 **国内网络友好**:博查(bocha)、百度千帆(qianfan)、百度(baidu)等源在国内可达\n- 📐 **归一化结果**:统一为 `{query, provider, count, results:[{title,url,snippet}], answer}`\n- 🛡️ **失败转 error**:provider/HTTP 失败不抛异常,转 `{error}` 让 agent 适配\n- 🔑 **多级密钥解析**:`options > config > 环境变量` 优先级\n- 🧩 **全集成点接线**:契约 schema、策略(只读/auto/plan-mode 允许)、权限规则(`WebSearch` 伞名)、子代理 profile、agent-core 工具分发\n\n## 系统架构\n\n```\n┌─────────────────────────────── cc agent ───────────────────────────────┐\n│ 工具循环 → case \"web_search\" → 读 webSearch config │\n└────────────────────────────────┬───────────────────────────────────────┘\n │\n ┌─────────▼──────────┐\n │ src/lib/web-search.js│ webSearch(query, opts)\n │ resolveProvider │ options>config>env\n │ resolveApiKey │\n │ 归一化 {results,…} │ 失败 → {error}\n └─────────┬───────────┘\n ┌──────────────────┼────────────────────┐\n ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐\n │ keyed │ │ keyless │ │ auto │\n │ tavily │ │ duckduckgo │ │ keyed 顺序 │\n │ brave │ │ searxng │ │ 择优,否则 │\n │ bocha │ │ baidu │ │ duckduckgo │\n │ qianfan │ └────────────┘ └────────────┘\n └────────────┘\n```\n\n集成点(与 `web_fetch` 一致):契约 `coding-agent-contract-shared.cjs`(schema + provider 枚举)、策略 `coding-agent-policy.cjs`(只读/auto/plan-mode 允许)、`permission-rules.cjs`(`WebSearch` 伞名 + `websearch` 分组)、`sub-agent-profiles.js`(explorer/executor/design 三 profile)、`agent-core.js`(`case \"web_search\"`)。\n\n## 搜索源参考\n\n### 带密钥(keyed)\n\n| 源 | 方式 | 说明 |\n| ----------------------------- | ---------------------------------------- | ------------------------------------------- |\n| `tavily` | POST | 带合成 `answer`,1000 次免费/月 |\n| `brave` | GET(`X-Subscription-Token`) | Brave Search API |\n| `bocha`(博查) | POST | Bing 风格 `data.webPages.value[]`,国内可达 |\n| `qianfan`(百度千帆 AI 搜索) | POST `qianfan.baidubce.com/v2/ai_search` | 解析 `references[]`,国内稳定 |\n\n### 免密钥(keyless)\n\n| 源 | 方式 | 说明 |\n| ------------ | ---- | ------------------------------------------------------- |\n| `duckduckgo` | HTML | **auto 默认回退**,解析 `result__a` + 解码 `uddg=` 跳转 |\n| `searxng` | JSON | 自建实例(`instanceUrl`) |\n| `baidu` | HTML | 抓 `www.baidu.com/s`,浏览器 UA,仅适合**低频** |\n\n### auto(默认)\n\n按 `tavily > brave > bocha > qianfan` 选已配置密钥的源;都没有 → 免密钥 `duckduckgo`。\n\n## 配置参考\n\n`.chainlesschain/config.json`:\n\n```json\n{\n \"webSearch\": {\n \"provider\": \"auto\",\n \"tavilyApiKey\": \"tvly-...\",\n \"braveApiKey\": \"...\",\n \"bochaApiKey\": \"sk-...\",\n \"qianfanApiKey\": \"\",\n \"qianfanUrl\": \"https://qianfan.baidubce.com/v2/ai_search\",\n \"searxng\": { \"instanceUrl\": \"https://searx.example.com\" }\n }\n}\n```\n\n环境变量(优先级 `options > config > env`):\n\n| 变量 | 源 |\n| -------------------------------------------- | ------- |\n| `TAVILY_API_KEY` | tavily |\n| `BRAVE_API_KEY`(或 `BRAVE_SEARCH_API_KEY`) | brave |\n| `BOCHA_API_KEY` | bocha |\n| `QIANFAN_API_KEY` | qianfan |\n\n> ⚠️ **qianfan token 格式**:`Authorization: Bearer `,token 必须是完整 `bce-v3/{access-key-id}/{secret}` 形式。只填短 API Key 片段 → `401 InvalidHTTPAuthHeader`;裸 key/`bce-v3/<短key>` → `401 Invalid token`。在千帆控制台开通「AI 搜索」获取完整 Bearer token,填 `webSearch.qianfanApiKey`,endpoint 可经 `webSearch.qianfanUrl` 覆盖。\n\n## 性能指标\n\n- **单次 HTTP**:每次搜索为一次后端 HTTP 请求 + 归一化解析,无额外开销。\n- **auto 零探测**:按已配置密钥静态择优,不做联网探测。\n- **失败快速降级**:provider/HTTP 失败立即转 `{error}`,不阻塞 agent 循环。\n\n## 测试覆盖率\n\n```bash\ncd packages/cli\nnpx vitest run __tests__/unit/web-search.test.js\n# 真后端联调(按 env 密钥 gated,无密钥则全 skip):\nBOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js\n```\n\n| 测试文件 | 数量 | 覆盖 |\n| ------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------- |\n| `web-search.test.js` | 21 | 密钥/源解析 + 5 适配器解析(含 ddg `uddg=` 解码 / baidu 验证码错误路径 / qianfan `references[]` / searxng 本地 server)+ 各 missing-key |\n| `web-search-live.test.js` | gated | 带密钥后端真查询(`it.skip` gated,无密钥不失败) |\n\n**真查实测**:DuckDuckGo(auto 回退,无密钥)查 2022 世界杯冠军 → 5 条正确结果(命中 Argentina)✅;qianfan(真 `bce-v3/…` token)查杭州亚运会 → `references[]` 解析正确 ✅;bocha(真 `sk-…` key)查 2024 诺贝尔物理学奖 → `data.webPages.value[]` 解析正确 ✅。\n\n## 安全考虑\n\n- **失败不泄露**:provider/HTTP 失败转明确 `{error}`(如 `missing API key`、`baidu: rate-limited / captcha challenge`),不静默失败、不抛栈。\n- **密钥仅本地**:搜索密钥存于本地 `webSearch` 配置或环境变量,不进入提示历史。\n- **权限收口**:`web_search` 受 `permission-rules.cjs` 的 `WebSearch` 伞名管理;只读/auto/plan-mode 策略允许,可按需收紧。\n- **低频保护**:免密钥 baidu 高频会触发反爬验证码,适配器专门检测 3xx/wappass 并返回明确限流错误而非静默——高频/稳定场景请用 keyed 源(qianfan/bocha)。\n\n## 故障排查\n\n| 现象 | 可能原因 | 处理 |\n| -------------------------------------------------- | ------------------------------------------------ | ---------------------------------------- |\n| qianfan 返回 `401 InvalidHTTPAuthHeader` | token 不是完整 `bce-v3/{access-key-id}/{secret}` | 用千帆控制台完整 Bearer token |\n| bocha 返回 `403 ... enough money or package quota` | 鉴权通过但余额/配额不足(非 401) | 充值后即恢复 |\n| baidu 返回 `rate-limited / captcha challenge` | 高频请求触发反爬 302→wappass | 降低频率,或改用 keyed 源 |\n| 返回 `missing API key` | 选了 keyed 源但未配密钥 | 配置对应密钥,或用 `duckduckgo`/`auto` |\n| auto 总是走 duckduckgo | 未配置任何 keyed 密钥 | 配置 tavily/brave/bocha/qianfan 任一密钥 |\n\n## 关键文件\n\n| 文件 | 说明 |\n| ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |\n| `packages/cli/src/lib/web-search.js` | 网络层:`webSearch(query, opts)` + 7 适配器 + `resolveProvider`/`resolveApiKey`(`_deps` 注入) |\n| `packages/cli/src/ai/agent-core.js` | `case \"web_search\"` 工具分发(读 `webSearch` config) |\n| `coding-agent-contract-shared.cjs` | 工具 schema + provider 枚举 |\n| `coding-agent-policy.cjs` · `permission-rules.cjs` | 策略允许 + `WebSearch` 伞名权限 |\n| `packages/cli/__tests__/unit/web-search.test.js` · `integration/web-search-live.test.js` | 21 单元 + gated 真查 |\n\n## 使用示例\n\n```bash\n# 1) 无密钥即可联网搜索(auto → duckduckgo)\ncc agent -p \"搜一下 2024 年诺贝尔物理学奖得主,并总结他们的贡献\"\n\n# 2) 配置带密钥的源(.chainlesschain/config.json)\n# \"webSearch\": { \"provider\": \"auto\", \"tavilyApiKey\": \"tvly-...\" }\ncc agent -p \"查最新的 Electron 安全公告\"\n\n# 3) 国内网络用千帆 / 博查\n# \"webSearch\": { \"provider\": \"qianfan\",\n# \"qianfanApiKey\": \"\" }\ncc agent -p \"杭州亚运会的举办时间和主要赛事\"\n```\n\n工具内部调用(归一化返回):\n\n```js\nconst { webSearch } = require(\"./lib/web-search.js\");\nconst r = await webSearch(\"who won the 2022 FIFA World Cup\", {\n provider: \"auto\",\n});\n// → { query, provider: \"duckduckgo\", count: 5,\n// results: [{ title, url, snippet }, ...], answer? }\n```\n\n## 相关文档\n\n- [CLI Agent 智能代理](./cli-agent.md)\n- [浏览器自动化](./browser-automation.md)\n- [计算机操控](./computer-use.md)\n- [权限系统](./permissions.md)\n- [MCP 集成](./cli-mcp.md)\n"}, "4db48500206ff6a789c53c8197933113b06a6097": {"t": 1785846923.1866686, "n": 9429, "h": "00d8d6dc522b175b", "c": "# 联网搜索工具(web_search)\n\n> **版本: v0.162+ | 状态: ✅ 生产可用 | 7 个可插拔搜索源 | 工具数 17→18 | 21 单元测试 + 真查实测**\n>\n> `web_search` 是 `cc agent` 的内置工具,补齐 agent 此前「能 `web_fetch` 抓取、却不能搜索」的能力缺口。后端**可插拔**——通过 `.chainlesschain/config.json` 的 `webSearch` 字段在 7 个搜索源之间切换,`auto` 模式按已配置的密钥自动择优,无密钥时回退到免费源。\n\n## 概述\n\nagent 要回答「最新发生了什么」就需要联网搜索。`web_search` 接收查询词,返回归一化的结果列表 `{title, url, snippet}`(部分源附带合成 `answer`),供 agent 进一步推理或配合 `web_fetch` 深读。它的设计镜像 `web_fetch`:网络层 `src/lib/web-search.js` 单点实现,provider/HTTP 失败**不抛异常**而转为 `{error}`,让 agent 自行适配重试或换源。\n\n搜索源是**配置开关**而非写死:你可以只用免费的 DuckDuckGo,也可以配置 Tavily/Brave/博查/千帆等带密钥的源以获得更稳定、更适合国内网络的结果。\n\n## 核心特性\n\n- 🔌 **7 个可插拔搜索源**:4 个带密钥 + 3 个免密钥,经 `webSearch` 配置切换\n- 🤖 **auto 自动择优**:按 `tavily > brave > bocha > qianfan` 顺序选已配置密钥的源,都没有则回退免费 `duckduckgo`\n- 🆓 **免密钥可用**:开箱即用,无需任何密钥即可联网搜索\n- 🌐 **国内网络友好**:博查(bocha)、百度千帆(qianfan)、百度(baidu)等源在国内可达\n- 📐 **归一化结果**:统一为 `{query, provider, count, results:[{title,url,snippet}], answer}`\n- 🛡️ **失败转 error**:provider/HTTP 失败不抛异常,转 `{error}` 让 agent 适配\n- 🔑 **多级密钥解析**:`options > config > 环境变量` 优先级\n- 🧩 **全集成点接线**:契约 schema、策略(只读/auto/plan-mode 允许)、权限规则(`WebSearch` 伞名)、子代理 profile、agent-core 工具分发\n\n## 系统架构\n\n```\n┌─────────────────────────────── cc agent ───────────────────────────────┐\n│ 工具循环 → case \"web_search\" → 读 webSearch config │\n└────────────────────────────────┬───────────────────────────────────────┘\n │\n ┌─────────▼──────────┐\n │ src/lib/web-search.js│ webSearch(query, opts)\n │ resolveProvider │ options>config>env\n │ resolveApiKey │\n │ 归一化 {results,…} │ 失败 → {error}\n └─────────┬───────────┘\n ┌──────────────────┼────────────────────┐\n ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐\n │ keyed │ │ keyless │ │ auto │\n │ tavily │ │ duckduckgo │ │ keyed 顺序 │\n │ brave │ │ searxng │ │ 择优,否则 │\n │ bocha │ │ baidu │ │ duckduckgo │\n │ qianfan │ └────────────┘ └────────────┘\n └────────────┘\n```\n\n集成点(与 `web_fetch` 一致):契约 `coding-agent-contract-shared.cjs`(schema + provider 枚举)、策略 `coding-agent-policy.cjs`(只读/auto/plan-mode 允许)、`permission-rules.cjs`(`WebSearch` 伞名 + `websearch` 分组)、`sub-agent-profiles.js`(explorer/executor/design 三 profile)、`agent-core.js`(`case \"web_search\"`)。\n\n## 搜索源参考\n\n### 带密钥(keyed)\n\n| 源 | 方式 | 说明 |\n| ----------------------------- | ---------------------------------------- | ------------------------------------------- |\n| `tavily` | POST | 带合成 `answer`,1000 次免费/月 |\n| `brave` | GET(`X-Subscription-Token`) | Brave Search API |\n| `bocha`(博查) | POST | Bing 风格 `data.webPages.value[]`,国内可达 |\n| `qianfan`(百度千帆 AI 搜索) | POST `qianfan.baidubce.com/v2/ai_search` | 解析 `references[]`,国内稳定 |\n\n### 免密钥(keyless)\n\n| 源 | 方式 | 说明 |\n| ------------ | ---- | ------------------------------------------------------- |\n| `duckduckgo` | HTML | **auto 默认回退**,解析 `result__a` + 解码 `uddg=` 跳转 |\n| `searxng` | JSON | 自建实例(`instanceUrl`) |\n| `baidu` | HTML | 抓 `www.baidu.com/s`,浏览器 UA,仅适合**低频** |\n\n### auto(默认)\n\n按 `tavily > brave > bocha > qianfan` 选已配置密钥的源;都没有 → 免密钥 `duckduckgo`。\n\n## 配置参考\n\n`.chainlesschain/config.json`:\n\n```json\n{\n \"webSearch\": {\n \"provider\": \"auto\",\n \"tavilyApiKey\": \"tvly-...\",\n \"braveApiKey\": \"...\",\n \"bochaApiKey\": \"sk-...\",\n \"qianfanApiKey\": \"\",\n \"qianfanUrl\": \"https://qianfan.baidubce.com/v2/ai_search\",\n \"searxng\": { \"instanceUrl\": \"https://searx.example.com\" }\n }\n}\n```\n\n环境变量(优先级 `options > config > env`):\n\n| 变量 | 源 |\n| -------------------------------------------- | ------- |\n| `TAVILY_API_KEY` | tavily |\n| `BRAVE_API_KEY`(或 `BRAVE_SEARCH_API_KEY`) | brave |\n| `BOCHA_API_KEY` | bocha |\n| `QIANFAN_API_KEY` | qianfan |\n\n> ⚠️ **qianfan token 格式**:`Authorization: Bearer `,token 必须是完整 `bce-v3/{access-key-id}/{secret}` 形式。只填短 API Key 片段 → `401 InvalidHTTPAuthHeader`;裸 key/`bce-v3/<短key>` → `401 Invalid token`。在千帆控制台开通「AI 搜索」获取完整 Bearer token,填 `webSearch.qianfanApiKey`,endpoint 可经 `webSearch.qianfanUrl` 覆盖。\n\n## 性能指标\n\n- **单次 HTTP**:每次搜索为一次后端 HTTP 请求 + 归一化解析,无额外开销。\n- **auto 零探测**:按已配置密钥静态择优,不做联网探测。\n- **失败快速降级**:provider/HTTP 失败立即转 `{error}`,不阻塞 agent 循环。\n\n## 测试覆盖率\n\n```bash\ncd packages/cli\nnpx vitest run __tests__/unit/web-search.test.js\n# 真后端联调(按 env 密钥 gated,无密钥则全 skip):\nBOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js\n```\n\n| 测试文件 | 数量 | 覆盖 |\n| ------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------- |\n| `web-search.test.js` | 21 | 密钥/源解析 + 5 适配器解析(含 ddg `uddg=` 解码 / baidu 验证码错误路径 / qianfan `references[]` / searxng 本地 server)+ 各 missing-key |\n| `web-search-live.test.js` | gated | 带密钥后端真查询(`it.skip` gated,无密钥不失败) |\n\n**真查实测**:DuckDuckGo(auto 回退,无密钥)查 2022 世界杯冠军 → 5 条正确结果(命中 Argentina)✅;qianfan(真 `bce-v3/…` token)查杭州亚运会 → `references[]` 解析正确 ✅;bocha(真 `sk-…` key)查 2024 诺贝尔物理学奖 → `data.webPages.value[]` 解析正确 ✅。\n\n## 安全考虑\n\n- **失败不泄露**:provider/HTTP 失败转明确 `{error}`(如 `missing API key`、`baidu: rate-limited / captcha challenge`),不静默失败、不抛栈。\n- **密钥仅本地**:搜索密钥存于本地 `webSearch` 配置或环境变量,不进入提示历史。\n- **权限收口**:`web_search` 受 `permission-rules.cjs` 的 `WebSearch` 伞名管理;只读/auto/plan-mode 策略允许,可按需收紧。\n- **低频保护**:免密钥 baidu 高频会触发反爬验证码,适配器专门检测 3xx/wappass 并返回明确限流错误而非静默——高频/稳定场景请用 keyed 源(qianfan/bocha)。\n\n## 故障排查\n\n| 现象 | 可能原因 | 处理 |\n| -------------------------------------------------- | ------------------------------------------------ | ---------------------------------------- |\n| qianfan 返回 `401 InvalidHTTPAuthHeader` | token 不是完整 `bce-v3/{access-key-id}/{secret}` | 用千帆控制台完整 Bearer token |\n| bocha 返回 `403 ... enough money or package quota` | 鉴权通过但余额/配额不足(非 401) | 充值后即恢复 |\n| baidu 返回 `rate-limited / captcha challenge` | 高频请求触发反爬 302→wappass | 降低频率,或改用 keyed 源 |\n| 返回 `missing API key` | 选了 keyed 源但未配密钥 | 配置对应密钥,或用 `duckduckgo`/`auto` |\n| auto 总是走 duckduckgo | 未配置任何 keyed 密钥 | 配置 tavily/brave/bocha/qianfan 任一密钥 |\n\n## 关键文件\n\n| 文件 | 说明 |\n| ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |\n| `packages/cli/src/lib/web-search.js` | 网络层:`webSearch(query, opts)` + 7 适配器 + `resolveProvider`/`resolveApiKey`(`_deps` 注入) |\n| `packages/cli/src/ai/agent-core.js` | `case \"web_search\"` 工具分发(读 `webSearch` config) |\n| `coding-agent-contract-shared.cjs` | 工具 schema + provider 枚举 |\n| `coding-agent-policy.cjs` · `permission-rules.cjs` | 策略允许 + `WebSearch` 伞名权限 |\n| `packages/cli/__tests__/unit/web-search.test.js` · `integration/web-search-live.test.js` | 21 单元 + gated 真查 |\n\n## 使用示例\n\n```bash\n# 1) 无密钥即可联网搜索(auto → duckduckgo)\ncc agent -p \"搜一下 2024 年诺贝尔物理学奖得主,并总结他们的贡献\"\n\n# 2) 配置带密钥的源(.chainlesschain/config.json)\n# \"webSearch\": { \"provider\": \"auto\", \"tavilyApiKey\": \"tvly-...\" }\ncc agent -p \"查最新的 Electron 安全公告\"\n\n# 3) 国内网络用千帆 / 博查\n# \"webSearch\": { \"provider\": \"qianfan\",\n# \"qianfanApiKey\": \"\" }\ncc agent -p \"杭州亚运会的举办时间和主要赛事\"\n```\n\n工具内部调用(归一化返回):\n\n```js\nconst { webSearch } = require(\"./lib/web-search.js\");\nconst r = await webSearch(\"who won the 2022 FIFA World Cup\", {\n provider: \"auto\",\n});\n// → { query, provider: \"duckduckgo\", count: 5,\n// results: [{ title, url, snippet }, ...], answer? }\n```\n\n## 相关文档\n\n- [CLI Agent 智能代理](./cli-agent.md)\n- [浏览器自动化](./browser-automation.md)\n- [计算机操控](./computer-use.md)\n- [权限系统](./permissions.md)\n- [MCP 集成](./cli-mcp.md)\n"}}, "packages/cli/src/lib/__tests__/provider-stream.test.js": {"9e2a3238426499a3de1d228034e66dab91cbfa2c": {"t": 1785745544.262289, "n": 5920, "h": "332d3aa3b864c6ca", "c": "\"use strict\";\n\n/**\n * provider-stream tests (previously untested) — the token-stream parsers behind\n * `cc stream` and the Hosted Session stream.run route. Pins the bug-prone\n * streaming parse logic: Ollama NDJSON (yield response, stop on done, line\n * buffering across chunk boundaries, skip malformed) and OpenAI-style SSE\n * (yield delta.content, stop on [DONE], skip non-data/empty), error on non-ok,\n * plus buildProviderSource routing (ollama defaults / known-provider endpoint +\n * Authorization / unsupported / missing-key). fetch is stubbed; no network.\n */\n\nimport { describe, it, expect, afterEach, vi } from \"vitest\";\nimport {\n ollamaTokenStream,\n openAIStream,\n buildProviderSource,\n} from \"../provider-stream.js\";\n\nafterEach(() => {\n vi.unstubAllGlobals();\n vi.unstubAllEnvs();\n});\n\n/** A fake fetch whose response body streams the given string chunks. */\nfunction mockFetch(\n chunks,\n { ok = true, status = 200, statusText = \"OK\" } = {},\n) {\n return vi.fn(async () => {\n const enc = new TextEncoder();\n let i = 0;\n return {\n ok,\n status,\n statusText,\n body: {\n getReader: () => ({\n read: async () =>\n i < chunks.length\n ? { value: enc.encode(chunks[i++]), done: false }\n : { value: undefined, done: true },\n }),\n },\n };\n });\n}\n\nasync function collect(gen) {\n const out = [];\n for await (const t of gen) out.push(t);\n return out;\n}\n\n// --------------------------------------------------------------------------- //\n// ollamaTokenStream\n// --------------------------------------------------------------------------- //\ndescribe(\"ollamaTokenStream\", () => {\n it(\"yields responses across NDJSON lines and stops on done\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch([\n '{\"response\":\"He\"}\\n{\"response\":\"llo\"}\\n',\n '{\"response\":\"!\"}\\n{\"done\":true}\\n',\n '{\"response\":\"AFTER\"}\\n', // never read — returned on done\n ]),\n );\n const out = await collect(\n ollamaTokenStream({ baseUrl: \"http://x\", model: \"m\", prompt: \"p\" }),\n );\n expect(out).toEqual([\"He\", \"llo\", \"!\"]);\n });\n\n it(\"buffers a JSON line split across chunk boundaries\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch(['{\"resp', 'onse\":\"hi\"}\\n{\"done\":true}\\n']),\n );\n expect(\n await collect(\n ollamaTokenStream({ baseUrl: \"http://x\", model: \"m\", prompt: \"p\" }),\n ),\n ).toEqual([\"hi\"]);\n });\n\n it(\"skips malformed JSON lines\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch(['not json\\n{\"response\":\"ok\"}\\n{\"done\":true}\\n']),\n );\n expect(\n await collect(\n ollamaTokenStream({ baseUrl: \"http://x\", model: \"m\", prompt: \"p\" }),\n ),\n ).toEqual([\"ok\"]);\n });\n\n it(\"throws on a non-ok response\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch([], { ok: false, status: 500, statusText: \"Err\" }),\n );\n await expect(\n collect(\n ollamaTokenStream({ baseUrl: \"http://x\", model: \"m\", prompt: \"p\" }),\n ),\n ).rejects.toThrow(/500/);\n });\n});\n\n// --------------------------------------------------------------------------- //\n// openAIStream\n// --------------------------------------------------------------------------- //\ndescribe(\"openAIStream\", () => {\n const args = { baseUrl: \"http://x\", apiKey: \"k\", model: \"m\", prompt: \"p\" };\n\n it(\"yields delta.content from data lines and stops on [DONE]\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch([\n 'data: {\"choices\":[{\"delta\":{\"content\":\"A\"}}]}\\n',\n 'data: {\"choices\":[{\"delta\":{\"content\":\"B\"}}]}\\ndata: [DONE]\\n',\n 'data: {\"choices\":[{\"delta\":{\"content\":\"AFTER\"}}]}\\n',\n ]),\n );\n expect(await collect(openAIStream(args))).toEqual([\"A\", \"B\"]);\n });\n\n it(\"skips non-data, empty and content-less delta lines\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch([\n \": comment\\n\\n\",\n 'data: {\"choices\":[{\"delta\":{}}]}\\n', // no content\n 'data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}\\ndata: [DONE]\\n',\n ]),\n );\n expect(await collect(openAIStream(args))).toEqual([\"x\"]);\n });\n\n it(\"throws on a non-ok response\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch([], { ok: false, status: 401, statusText: \"No\" }),\n );\n await expect(collect(openAIStream(args))).rejects.toThrow(/401/);\n });\n});\n\n// --------------------------------------------------------------------------- //\n// buildProviderSource\n// --------------------------------------------------------------------------- //\ndescribe(\"buildProviderSource\", () => {\n it(\"routes ollama to /api/generate with localhost default\", async () => {\n const fetchMock = mockFetch(['{\"done\":true}\\n']);\n vi.stubGlobal(\"fetch\", fetchMock);\n await collect(buildProviderSource(\"ollama\", { prompt: \"hi\" }));\n expect(fetchMock).toHaveBeenCalledWith(\n \"http://localhost:11434/api/generate\",\n expect.objectContaining({ method: \"POST\" }),\n );\n });\n\n it(\"routes a known provider to /chat/completions with a Bearer key\", async () => {\n const fetchMock = mockFetch([\"data: [DONE]\\n\"]);\n vi.stubGlobal(\"fetch\", fetchMock);\n await collect(\n buildProviderSource(\"deepseek\", { apiKey: \"sk-x\", prompt: \"hi\" }),\n );\n expect(fetchMock).toHaveBeenCalledWith(\n \"https://api.deepseek.com/v1/chat/completions\",\n expect.objectContaining({\n headers: expect.objectContaining({ Authorization: \"Bearer sk-x\" }),\n }),\n );\n });\n\n it(\"throws for an unsupported provider\", () => {\n expect(() => buildProviderSource(\"nope\", { prompt: \"x\" })).toThrow(\n /Unsupported provider/,\n );\n });\n\n it(\"throws when a known provider has no api key\", () => {\n vi.stubEnv(\"DEEPSEEK_API_KEY\", \"\");\n expect(() => buildProviderSource(\"deepseek\", { prompt: \"x\" })).toThrow(\n /API key/,\n );\n });\n});\n"}, "37be7e4a254e9586076325529881cccfb4e69a79": {"t": 1785754067.7711327, "n": 5920, "h": "332d3aa3b864c6ca", "c": "\"use strict\";\n\n/**\n * provider-stream tests (previously untested) — the token-stream parsers behind\n * `cc stream` and the Hosted Session stream.run route. Pins the bug-prone\n * streaming parse logic: Ollama NDJSON (yield response, stop on done, line\n * buffering across chunk boundaries, skip malformed) and OpenAI-style SSE\n * (yield delta.content, stop on [DONE], skip non-data/empty), error on non-ok,\n * plus buildProviderSource routing (ollama defaults / known-provider endpoint +\n * Authorization / unsupported / missing-key). fetch is stubbed; no network.\n */\n\nimport { describe, it, expect, afterEach, vi } from \"vitest\";\nimport {\n ollamaTokenStream,\n openAIStream,\n buildProviderSource,\n} from \"../provider-stream.js\";\n\nafterEach(() => {\n vi.unstubAllGlobals();\n vi.unstubAllEnvs();\n});\n\n/** A fake fetch whose response body streams the given string chunks. */\nfunction mockFetch(\n chunks,\n { ok = true, status = 200, statusText = \"OK\" } = {},\n) {\n return vi.fn(async () => {\n const enc = new TextEncoder();\n let i = 0;\n return {\n ok,\n status,\n statusText,\n body: {\n getReader: () => ({\n read: async () =>\n i < chunks.length\n ? { value: enc.encode(chunks[i++]), done: false }\n : { value: undefined, done: true },\n }),\n },\n };\n });\n}\n\nasync function collect(gen) {\n const out = [];\n for await (const t of gen) out.push(t);\n return out;\n}\n\n// --------------------------------------------------------------------------- //\n// ollamaTokenStream\n// --------------------------------------------------------------------------- //\ndescribe(\"ollamaTokenStream\", () => {\n it(\"yields responses across NDJSON lines and stops on done\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch([\n '{\"response\":\"He\"}\\n{\"response\":\"llo\"}\\n',\n '{\"response\":\"!\"}\\n{\"done\":true}\\n',\n '{\"response\":\"AFTER\"}\\n', // never read — returned on done\n ]),\n );\n const out = await collect(\n ollamaTokenStream({ baseUrl: \"http://x\", model: \"m\", prompt: \"p\" }),\n );\n expect(out).toEqual([\"He\", \"llo\", \"!\"]);\n });\n\n it(\"buffers a JSON line split across chunk boundaries\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch(['{\"resp', 'onse\":\"hi\"}\\n{\"done\":true}\\n']),\n );\n expect(\n await collect(\n ollamaTokenStream({ baseUrl: \"http://x\", model: \"m\", prompt: \"p\" }),\n ),\n ).toEqual([\"hi\"]);\n });\n\n it(\"skips malformed JSON lines\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch(['not json\\n{\"response\":\"ok\"}\\n{\"done\":true}\\n']),\n );\n expect(\n await collect(\n ollamaTokenStream({ baseUrl: \"http://x\", model: \"m\", prompt: \"p\" }),\n ),\n ).toEqual([\"ok\"]);\n });\n\n it(\"throws on a non-ok response\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch([], { ok: false, status: 500, statusText: \"Err\" }),\n );\n await expect(\n collect(\n ollamaTokenStream({ baseUrl: \"http://x\", model: \"m\", prompt: \"p\" }),\n ),\n ).rejects.toThrow(/500/);\n });\n});\n\n// --------------------------------------------------------------------------- //\n// openAIStream\n// --------------------------------------------------------------------------- //\ndescribe(\"openAIStream\", () => {\n const args = { baseUrl: \"http://x\", apiKey: \"k\", model: \"m\", prompt: \"p\" };\n\n it(\"yields delta.content from data lines and stops on [DONE]\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch([\n 'data: {\"choices\":[{\"delta\":{\"content\":\"A\"}}]}\\n',\n 'data: {\"choices\":[{\"delta\":{\"content\":\"B\"}}]}\\ndata: [DONE]\\n',\n 'data: {\"choices\":[{\"delta\":{\"content\":\"AFTER\"}}]}\\n',\n ]),\n );\n expect(await collect(openAIStream(args))).toEqual([\"A\", \"B\"]);\n });\n\n it(\"skips non-data, empty and content-less delta lines\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch([\n \": comment\\n\\n\",\n 'data: {\"choices\":[{\"delta\":{}}]}\\n', // no content\n 'data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}\\ndata: [DONE]\\n',\n ]),\n );\n expect(await collect(openAIStream(args))).toEqual([\"x\"]);\n });\n\n it(\"throws on a non-ok response\", async () => {\n vi.stubGlobal(\n \"fetch\",\n mockFetch([], { ok: false, status: 401, statusText: \"No\" }),\n );\n await expect(collect(openAIStream(args))).rejects.toThrow(/401/);\n });\n});\n\n// --------------------------------------------------------------------------- //\n// buildProviderSource\n// --------------------------------------------------------------------------- //\ndescribe(\"buildProviderSource\", () => {\n it(\"routes ollama to /api/generate with localhost default\", async () => {\n const fetchMock = mockFetch(['{\"done\":true}\\n']);\n vi.stubGlobal(\"fetch\", fetchMock);\n await collect(buildProviderSource(\"ollama\", { prompt: \"hi\" }));\n expect(fetchMock).toHaveBeenCalledWith(\n \"http://localhost:11434/api/generate\",\n expect.objectContaining({ method: \"POST\" }),\n );\n });\n\n it(\"routes a known provider to /chat/completions with a Bearer key\", async () => {\n const fetchMock = mockFetch([\"data: [DONE]\\n\"]);\n vi.stubGlobal(\"fetch\", fetchMock);\n await collect(\n buildProviderSource(\"deepseek\", { apiKey: \"sk-x\", prompt: \"hi\" }),\n );\n expect(fetchMock).toHaveBeenCalledWith(\n \"https://api.deepseek.com/v1/chat/completions\",\n expect.objectContaining({\n headers: expect.objectContaining({ Authorization: \"Bearer sk-x\" }),\n }),\n );\n });\n\n it(\"throws for an unsupported provider\", () => {\n expect(() => buildProviderSource(\"nope\", { prompt: \"x\" })).toThrow(\n /Unsupported provider/,\n );\n });\n\n it(\"throws when a known provider has no api key\", () => {\n vi.stubEnv(\"DEEPSEEK_API_KEY\", \"\");\n expect(() => buildProviderSource(\"deepseek\", { prompt: \"x\" })).toThrow(\n /API key/,\n );\n });\n});\n"}}, "android-app/core-security/src/main/java/com/chainlesschain/android/core/security/SecurePreferences.kt": {"9e2a3238426499a3de1d228034e66dab91cbfa2c": {"t": 1785745712.5996737, "n": 4720, "h": "2dc81659a2cd6bf4", "c": "package com.chainlesschain.android.core.security\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * 安全偏好设置(加密存储)\n *\n * 使用EncryptedSharedPreferences存储敏感数据\n * - API Keys\n * - Tokens\n * - 其他敏感配置\n */\n@Singleton\nclass SecurePreferences @Inject constructor(\n @ApplicationContext private val context: Context\n) {\n private val masterKey: MasterKey = MasterKey.Builder(context)\n .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n .build()\n\n private val encryptedPrefs: SharedPreferences = EncryptedSharedPreferences.create(\n context,\n PREFS_FILENAME,\n masterKey,\n EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM\n )\n\n companion object {\n private const val PREFS_FILENAME = \"chainlesschain_secure_prefs\"\n\n // Keys\n private const val KEY_OPENAI_API_KEY = \"openai_api_key\"\n private const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n private const val KEY_CUSTOM_API_KEY = \"custom_api_key\"\n private const val KEY_CUSTOM_API_ENDPOINT = \"custom_api_endpoint\"\n private const val KEY_OLLAMA_BASE_URL = \"ollama_base_url\"\n\n // 通用提供商API Key前缀\n private const val KEY_PROVIDER_PREFIX = \"provider_api_key_\"\n }\n\n /**\n * 保存OpenAI API Key\n */\n fun saveOpenAIApiKey(apiKey: String) {\n encryptedPrefs.edit()\n .putString(KEY_OPENAI_API_KEY, apiKey)\n .apply()\n }\n\n /**\n * 获取OpenAI API Key\n */\n fun getOpenAIApiKey(): String? {\n return encryptedPrefs.getString(KEY_OPENAI_API_KEY, null)\n }\n\n /**\n * 保存DeepSeek API Key\n */\n fun saveDeepSeekApiKey(apiKey: String) {\n encryptedPrefs.edit()\n .putString(KEY_DEEPSEEK_API_KEY, apiKey)\n .apply()\n }\n\n /**\n * 获取DeepSeek API Key\n */\n fun getDeepSeekApiKey(): String? {\n return encryptedPrefs.getString(KEY_DEEPSEEK_API_KEY, null)\n }\n\n /**\n * 保存自定义API Key\n */\n fun saveCustomApiKey(apiKey: String) {\n encryptedPrefs.edit()\n .putString(KEY_CUSTOM_API_KEY, apiKey)\n .apply()\n }\n\n /**\n * 获取自定义API Key\n */\n fun getCustomApiKey(): String? {\n return encryptedPrefs.getString(KEY_CUSTOM_API_KEY, null)\n }\n\n /**\n * 保存自定义API端点\n */\n fun saveCustomApiEndpoint(endpoint: String) {\n encryptedPrefs.edit()\n .putString(KEY_CUSTOM_API_ENDPOINT, endpoint)\n .apply()\n }\n\n /**\n * 获取自定义API端点\n */\n fun getCustomApiEndpoint(): String? {\n return encryptedPrefs.getString(KEY_CUSTOM_API_ENDPOINT, null)\n }\n\n /**\n * 根据提供商获取API Key\n */\n fun getApiKeyForProvider(provider: String): String? {\n return when (provider.uppercase()) {\n \"OPENAI\" -> getOpenAIApiKey()\n \"DEEPSEEK\" -> getDeepSeekApiKey()\n \"CUSTOM\" -> getCustomApiKey()\n else -> {\n // 使用通用存储获取其他提供商的API Key\n encryptedPrefs.getString(\"$KEY_PROVIDER_PREFIX${provider.lowercase()}\", null)\n }\n }\n }\n\n /**\n * 根据提供商保存API Key\n */\n fun saveApiKeyForProvider(provider: String, apiKey: String) {\n when (provider.uppercase()) {\n \"OPENAI\" -> saveOpenAIApiKey(apiKey)\n \"DEEPSEEK\" -> saveDeepSeekApiKey(apiKey)\n \"CUSTOM\" -> saveCustomApiKey(apiKey)\n else -> {\n // 使用通用存储保存其他提供商的API Key\n encryptedPrefs.edit()\n .putString(\"$KEY_PROVIDER_PREFIX${provider.lowercase()}\", apiKey)\n .apply()\n }\n }\n }\n\n /**\n * 保存Ollama基础URL\n */\n fun saveOllamaBaseUrl(url: String) {\n encryptedPrefs.edit()\n .putString(KEY_OLLAMA_BASE_URL, url)\n .apply()\n }\n\n /**\n * 获取Ollama基础URL\n */\n fun getOllamaBaseUrl(): String? {\n return encryptedPrefs.getString(KEY_OLLAMA_BASE_URL, null)\n }\n\n /**\n * 删除所有API Keys\n */\n fun clearAllApiKeys() {\n encryptedPrefs.edit()\n .remove(KEY_OPENAI_API_KEY)\n .remove(KEY_DEEPSEEK_API_KEY)\n .remove(KEY_CUSTOM_API_KEY)\n .remove(KEY_CUSTOM_API_ENDPOINT)\n .apply()\n }\n\n /**\n * 检查是否有已保存的API Key\n */\n fun hasApiKeyForProvider(provider: String): Boolean {\n return getApiKeyForProvider(provider)?.isNotEmpty() == true\n }\n}\n"}, "3b0b2dc09dceaf104b65bdb7bdc4448fc55e53a5": {"t": 1785754067.3947997, "n": 4720, "h": "2dc81659a2cd6bf4", "c": "package com.chainlesschain.android.core.security\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * 安全偏好设置(加密存储)\n *\n * 使用EncryptedSharedPreferences存储敏感数据\n * - API Keys\n * - Tokens\n * - 其他敏感配置\n */\n@Singleton\nclass SecurePreferences @Inject constructor(\n @ApplicationContext private val context: Context\n) {\n private val masterKey: MasterKey = MasterKey.Builder(context)\n .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n .build()\n\n private val encryptedPrefs: SharedPreferences = EncryptedSharedPreferences.create(\n context,\n PREFS_FILENAME,\n masterKey,\n EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM\n )\n\n companion object {\n private const val PREFS_FILENAME = \"chainlesschain_secure_prefs\"\n\n // Keys\n private const val KEY_OPENAI_API_KEY = \"openai_api_key\"\n private const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n private const val KEY_CUSTOM_API_KEY = \"custom_api_key\"\n private const val KEY_CUSTOM_API_ENDPOINT = \"custom_api_endpoint\"\n private const val KEY_OLLAMA_BASE_URL = \"ollama_base_url\"\n\n // 通用提供商API Key前缀\n private const val KEY_PROVIDER_PREFIX = \"provider_api_key_\"\n }\n\n /**\n * 保存OpenAI API Key\n */\n fun saveOpenAIApiKey(apiKey: String) {\n encryptedPrefs.edit()\n .putString(KEY_OPENAI_API_KEY, apiKey)\n .apply()\n }\n\n /**\n * 获取OpenAI API Key\n */\n fun getOpenAIApiKey(): String? {\n return encryptedPrefs.getString(KEY_OPENAI_API_KEY, null)\n }\n\n /**\n * 保存DeepSeek API Key\n */\n fun saveDeepSeekApiKey(apiKey: String) {\n encryptedPrefs.edit()\n .putString(KEY_DEEPSEEK_API_KEY, apiKey)\n .apply()\n }\n\n /**\n * 获取DeepSeek API Key\n */\n fun getDeepSeekApiKey(): String? {\n return encryptedPrefs.getString(KEY_DEEPSEEK_API_KEY, null)\n }\n\n /**\n * 保存自定义API Key\n */\n fun saveCustomApiKey(apiKey: String) {\n encryptedPrefs.edit()\n .putString(KEY_CUSTOM_API_KEY, apiKey)\n .apply()\n }\n\n /**\n * 获取自定义API Key\n */\n fun getCustomApiKey(): String? {\n return encryptedPrefs.getString(KEY_CUSTOM_API_KEY, null)\n }\n\n /**\n * 保存自定义API端点\n */\n fun saveCustomApiEndpoint(endpoint: String) {\n encryptedPrefs.edit()\n .putString(KEY_CUSTOM_API_ENDPOINT, endpoint)\n .apply()\n }\n\n /**\n * 获取自定义API端点\n */\n fun getCustomApiEndpoint(): String? {\n return encryptedPrefs.getString(KEY_CUSTOM_API_ENDPOINT, null)\n }\n\n /**\n * 根据提供商获取API Key\n */\n fun getApiKeyForProvider(provider: String): String? {\n return when (provider.uppercase()) {\n \"OPENAI\" -> getOpenAIApiKey()\n \"DEEPSEEK\" -> getDeepSeekApiKey()\n \"CUSTOM\" -> getCustomApiKey()\n else -> {\n // 使用通用存储获取其他提供商的API Key\n encryptedPrefs.getString(\"$KEY_PROVIDER_PREFIX${provider.lowercase()}\", null)\n }\n }\n }\n\n /**\n * 根据提供商保存API Key\n */\n fun saveApiKeyForProvider(provider: String, apiKey: String) {\n when (provider.uppercase()) {\n \"OPENAI\" -> saveOpenAIApiKey(apiKey)\n \"DEEPSEEK\" -> saveDeepSeekApiKey(apiKey)\n \"CUSTOM\" -> saveCustomApiKey(apiKey)\n else -> {\n // 使用通用存储保存其他提供商的API Key\n encryptedPrefs.edit()\n .putString(\"$KEY_PROVIDER_PREFIX${provider.lowercase()}\", apiKey)\n .apply()\n }\n }\n }\n\n /**\n * 保存Ollama基础URL\n */\n fun saveOllamaBaseUrl(url: String) {\n encryptedPrefs.edit()\n .putString(KEY_OLLAMA_BASE_URL, url)\n .apply()\n }\n\n /**\n * 获取Ollama基础URL\n */\n fun getOllamaBaseUrl(): String? {\n return encryptedPrefs.getString(KEY_OLLAMA_BASE_URL, null)\n }\n\n /**\n * 删除所有API Keys\n */\n fun clearAllApiKeys() {\n encryptedPrefs.edit()\n .remove(KEY_OPENAI_API_KEY)\n .remove(KEY_DEEPSEEK_API_KEY)\n .remove(KEY_CUSTOM_API_KEY)\n .remove(KEY_CUSTOM_API_ENDPOINT)\n .apply()\n }\n\n /**\n * 检查是否有已保存的API Key\n */\n fun hasApiKeyForProvider(provider: String): Boolean {\n return getApiKeyForProvider(provider)?.isNotEmpty() == true\n }\n}\n"}, "c3fe912fefd60bb6332748f59241fb6de61e9c5b": {"t": 1785754070.0619793, "n": 4720, "h": "2dc81659a2cd6bf4", "c": "package com.chainlesschain.android.core.security\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * 安全偏好设置(加密存储)\n *\n * 使用EncryptedSharedPreferences存储敏感数据\n * - API Keys\n * - Tokens\n * - 其他敏感配置\n */\n@Singleton\nclass SecurePreferences @Inject constructor(\n @ApplicationContext private val context: Context\n) {\n private val masterKey: MasterKey = MasterKey.Builder(context)\n .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n .build()\n\n private val encryptedPrefs: SharedPreferences = EncryptedSharedPreferences.create(\n context,\n PREFS_FILENAME,\n masterKey,\n EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM\n )\n\n companion object {\n private const val PREFS_FILENAME = \"chainlesschain_secure_prefs\"\n\n // Keys\n private const val KEY_OPENAI_API_KEY = \"openai_api_key\"\n private const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n private const val KEY_CUSTOM_API_KEY = \"custom_api_key\"\n private const val KEY_CUSTOM_API_ENDPOINT = \"custom_api_endpoint\"\n private const val KEY_OLLAMA_BASE_URL = \"ollama_base_url\"\n\n // 通用提供商API Key前缀\n private const val KEY_PROVIDER_PREFIX = \"provider_api_key_\"\n }\n\n /**\n * 保存OpenAI API Key\n */\n fun saveOpenAIApiKey(apiKey: String) {\n encryptedPrefs.edit()\n .putString(KEY_OPENAI_API_KEY, apiKey)\n .apply()\n }\n\n /**\n * 获取OpenAI API Key\n */\n fun getOpenAIApiKey(): String? {\n return encryptedPrefs.getString(KEY_OPENAI_API_KEY, null)\n }\n\n /**\n * 保存DeepSeek API Key\n */\n fun saveDeepSeekApiKey(apiKey: String) {\n encryptedPrefs.edit()\n .putString(KEY_DEEPSEEK_API_KEY, apiKey)\n .apply()\n }\n\n /**\n * 获取DeepSeek API Key\n */\n fun getDeepSeekApiKey(): String? {\n return encryptedPrefs.getString(KEY_DEEPSEEK_API_KEY, null)\n }\n\n /**\n * 保存自定义API Key\n */\n fun saveCustomApiKey(apiKey: String) {\n encryptedPrefs.edit()\n .putString(KEY_CUSTOM_API_KEY, apiKey)\n .apply()\n }\n\n /**\n * 获取自定义API Key\n */\n fun getCustomApiKey(): String? {\n return encryptedPrefs.getString(KEY_CUSTOM_API_KEY, null)\n }\n\n /**\n * 保存自定义API端点\n */\n fun saveCustomApiEndpoint(endpoint: String) {\n encryptedPrefs.edit()\n .putString(KEY_CUSTOM_API_ENDPOINT, endpoint)\n .apply()\n }\n\n /**\n * 获取自定义API端点\n */\n fun getCustomApiEndpoint(): String? {\n return encryptedPrefs.getString(KEY_CUSTOM_API_ENDPOINT, null)\n }\n\n /**\n * 根据提供商获取API Key\n */\n fun getApiKeyForProvider(provider: String): String? {\n return when (provider.uppercase()) {\n \"OPENAI\" -> getOpenAIApiKey()\n \"DEEPSEEK\" -> getDeepSeekApiKey()\n \"CUSTOM\" -> getCustomApiKey()\n else -> {\n // 使用通用存储获取其他提供商的API Key\n encryptedPrefs.getString(\"$KEY_PROVIDER_PREFIX${provider.lowercase()}\", null)\n }\n }\n }\n\n /**\n * 根据提供商保存API Key\n */\n fun saveApiKeyForProvider(provider: String, apiKey: String) {\n when (provider.uppercase()) {\n \"OPENAI\" -> saveOpenAIApiKey(apiKey)\n \"DEEPSEEK\" -> saveDeepSeekApiKey(apiKey)\n \"CUSTOM\" -> saveCustomApiKey(apiKey)\n else -> {\n // 使用通用存储保存其他提供商的API Key\n encryptedPrefs.edit()\n .putString(\"$KEY_PROVIDER_PREFIX${provider.lowercase()}\", apiKey)\n .apply()\n }\n }\n }\n\n /**\n * 保存Ollama基础URL\n */\n fun saveOllamaBaseUrl(url: String) {\n encryptedPrefs.edit()\n .putString(KEY_OLLAMA_BASE_URL, url)\n .apply()\n }\n\n /**\n * 获取Ollama基础URL\n */\n fun getOllamaBaseUrl(): String? {\n return encryptedPrefs.getString(KEY_OLLAMA_BASE_URL, null)\n }\n\n /**\n * 删除所有API Keys\n */\n fun clearAllApiKeys() {\n encryptedPrefs.edit()\n .remove(KEY_OPENAI_API_KEY)\n .remove(KEY_DEEPSEEK_API_KEY)\n .remove(KEY_CUSTOM_API_KEY)\n .remove(KEY_CUSTOM_API_ENDPOINT)\n .apply()\n }\n\n /**\n * 检查是否有已保存的API Key\n */\n fun hasApiKeyForProvider(provider: String): Boolean {\n return getApiKeyForProvider(provider)?.isNotEmpty() == true\n }\n}\n"}, "e2612b2f15916b59ffdb3c21a308f1a0e97bb940": {"t": 1785754072.6819966, "n": 4720, "h": "2dc81659a2cd6bf4", "c": "package com.chainlesschain.android.core.security\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * 安全偏好设置(加密存储)\n *\n * 使用EncryptedSharedPreferences存储敏感数据\n * - API Keys\n * - Tokens\n * - 其他敏感配置\n */\n@Singleton\nclass SecurePreferences @Inject constructor(\n @ApplicationContext private val context: Context\n) {\n private val masterKey: MasterKey = MasterKey.Builder(context)\n .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n .build()\n\n private val encryptedPrefs: SharedPreferences = EncryptedSharedPreferences.create(\n context,\n PREFS_FILENAME,\n masterKey,\n EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM\n )\n\n companion object {\n private const val PREFS_FILENAME = \"chainlesschain_secure_prefs\"\n\n // Keys\n private const val KEY_OPENAI_API_KEY = \"openai_api_key\"\n private const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n private const val KEY_CUSTOM_API_KEY = \"custom_api_key\"\n private const val KEY_CUSTOM_API_ENDPOINT = \"custom_api_endpoint\"\n private const val KEY_OLLAMA_BASE_URL = \"ollama_base_url\"\n\n // 通用提供商API Key前缀\n private const val KEY_PROVIDER_PREFIX = \"provider_api_key_\"\n }\n\n /**\n * 保存OpenAI API Key\n */\n fun saveOpenAIApiKey(apiKey: String) {\n encryptedPrefs.edit()\n .putString(KEY_OPENAI_API_KEY, apiKey)\n .apply()\n }\n\n /**\n * 获取OpenAI API Key\n */\n fun getOpenAIApiKey(): String? {\n return encryptedPrefs.getString(KEY_OPENAI_API_KEY, null)\n }\n\n /**\n * 保存DeepSeek API Key\n */\n fun saveDeepSeekApiKey(apiKey: String) {\n encryptedPrefs.edit()\n .putString(KEY_DEEPSEEK_API_KEY, apiKey)\n .apply()\n }\n\n /**\n * 获取DeepSeek API Key\n */\n fun getDeepSeekApiKey(): String? {\n return encryptedPrefs.getString(KEY_DEEPSEEK_API_KEY, null)\n }\n\n /**\n * 保存自定义API Key\n */\n fun saveCustomApiKey(apiKey: String) {\n encryptedPrefs.edit()\n .putString(KEY_CUSTOM_API_KEY, apiKey)\n .apply()\n }\n\n /**\n * 获取自定义API Key\n */\n fun getCustomApiKey(): String? {\n return encryptedPrefs.getString(KEY_CUSTOM_API_KEY, null)\n }\n\n /**\n * 保存自定义API端点\n */\n fun saveCustomApiEndpoint(endpoint: String) {\n encryptedPrefs.edit()\n .putString(KEY_CUSTOM_API_ENDPOINT, endpoint)\n .apply()\n }\n\n /**\n * 获取自定义API端点\n */\n fun getCustomApiEndpoint(): String? {\n return encryptedPrefs.getString(KEY_CUSTOM_API_ENDPOINT, null)\n }\n\n /**\n * 根据提供商获取API Key\n */\n fun getApiKeyForProvider(provider: String): String? {\n return when (provider.uppercase()) {\n \"OPENAI\" -> getOpenAIApiKey()\n \"DEEPSEEK\" -> getDeepSeekApiKey()\n \"CUSTOM\" -> getCustomApiKey()\n else -> {\n // 使用通用存储获取其他提供商的API Key\n encryptedPrefs.getString(\"$KEY_PROVIDER_PREFIX${provider.lowercase()}\", null)\n }\n }\n }\n\n /**\n * 根据提供商保存API Key\n */\n fun saveApiKeyForProvider(provider: String, apiKey: String) {\n when (provider.uppercase()) {\n \"OPENAI\" -> saveOpenAIApiKey(apiKey)\n \"DEEPSEEK\" -> saveDeepSeekApiKey(apiKey)\n \"CUSTOM\" -> saveCustomApiKey(apiKey)\n else -> {\n // 使用通用存储保存其他提供商的API Key\n encryptedPrefs.edit()\n .putString(\"$KEY_PROVIDER_PREFIX${provider.lowercase()}\", apiKey)\n .apply()\n }\n }\n }\n\n /**\n * 保存Ollama基础URL\n */\n fun saveOllamaBaseUrl(url: String) {\n encryptedPrefs.edit()\n .putString(KEY_OLLAMA_BASE_URL, url)\n .apply()\n }\n\n /**\n * 获取Ollama基础URL\n */\n fun getOllamaBaseUrl(): String? {\n return encryptedPrefs.getString(KEY_OLLAMA_BASE_URL, null)\n }\n\n /**\n * 删除所有API Keys\n */\n fun clearAllApiKeys() {\n encryptedPrefs.edit()\n .remove(KEY_OPENAI_API_KEY)\n .remove(KEY_DEEPSEEK_API_KEY)\n .remove(KEY_CUSTOM_API_KEY)\n .remove(KEY_CUSTOM_API_ENDPOINT)\n .apply()\n }\n\n /**\n * 检查是否有已保存的API Key\n */\n fun hasApiKeyForProvider(provider: String): Boolean {\n return getApiKeyForProvider(provider)?.isNotEmpty() == true\n }\n}\n"}}, "android-app/feature-local-terminal/src/main/java/com/chainlesschain/android/feature/localterminal/PtyEnvironment.kt": {"9e2a3238426499a3de1d228034e66dab91cbfa2c": {"t": 1785745735.6408813, "n": 13315, "h": "8742e6ef50e1dd72", "c": "package com.chainlesschain.android.feature.localterminal\n\nimport android.content.Context\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport timber.log.Timber\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Builds the env-var array passed to [LocalPtyClient.Config.envp] for shells\n * started against the `$PREFIX` layout produced by [LocalFilesystemBootstrapper].\n *\n * Centralising env construction here keeps a single source of truth — the\n * design doc §5 Trap 8 catalogues the PATH / TMPDIR / shebang gotchas; the\n * defaults below sidestep all of them.\n *\n * Phase 2 contract (subject to change in Phase 5 when termux-exec lands):\n *\n * PATH = $PREFIX/bin : /system/bin : /system/xbin\n * HOME = /home\n * TMPDIR = $PREFIX/tmp\n * SHELL = $PREFIX/bin/mksh\n * TERM = xterm-256color (overrideable)\n * LANG = en_US.UTF-8 (overrideable)\n * PREFIX = absolute path to $PREFIX (for scripts that need it)\n * ENV = $PREFIX/etc/profile (mksh loads on -l)\n *\n * Phase 2.5 — also injects LLM API keys from the app's encrypted prefs so\n * `cc ask`, `cc agent` etc work out of the box once the user has set a key\n * in the AI settings UI. See [loadLlmKeyEnvs] for the mapping. Empty keys\n * are skipped (no `KEY=` blank entry).\n *\n * Notable omissions until Phase 5:\n *\n * - **No `LD_PRELOAD=$PREFIX/lib/libtermux-exec.so`** — termux-exec\n * vendoring is deferred to Sub-phase 5.4 (only matters when bundled\n * scripts use shebangs like `#!/usr/bin/env python`).\n */\n@Singleton\nclass PtyEnvironment @Inject constructor(\n @ApplicationContext private val context: Context,\n private val bootstrapper: LocalFilesystemBootstrapper,\n) {\n\n /**\n * Build env-var array suitable for [LocalPtyClient.Config.envp].\n *\n * @param extra additional or overriding env entries. `extra` keys\n * override defaults; e.g. `mapOf(\"TERM\" to \"dumb\")` for\n * tests that don't want xterm escapes.\n * @return Array of `KEY=VALUE` strings.\n */\n fun envp(extra: Map = emptyMap()): Array {\n val prefix = bootstrapper.prefixDir.absolutePath\n val home = bootstrapper.homeDir.absolutePath\n val tmp = bootstrapper.tmpDir.absolutePath\n\n val defaults = mapOf(\n \"PATH\" to \"$prefix/bin:/system/bin:/system/xbin\",\n \"HOME\" to home,\n \"TMPDIR\" to tmp,\n \"SHELL\" to \"$prefix/bin/mksh\",\n \"TERM\" to \"xterm-256color\",\n \"LANG\" to \"en_US.UTF-8\",\n \"PREFIX\" to prefix,\n \"ENV\" to \"$prefix/etc/profile\",\n // Phase 2.5 — Node.js DT_RUNPATH was patched to $ORIGIN, so libnode.so\n // finds libcrypto/libssl/libicu* in the same lib// dir. But\n // libnode itself is exec'd via the `$PREFIX/bin/node` symlink whose\n // parent dir is bin/ — to keep the loader happy across symlink\n // chains, also set LD_LIBRARY_PATH explicitly.\n \"LD_LIBRARY_PATH\" to \"$prefix/lib\",\n // Where global node_modules live — chainlesschain CLI is installed\n // under $PREFIX/lib/node_modules/, so `require()` resolution from\n // the shim binary picks it up.\n \"NODE_PATH\" to \"$prefix/lib/node_modules\",\n // `cc ui` defaults — bind 0.0.0.0 so LAN devices on the same WiFi\n // can hit the management UI without adb forward. Auto-token avoids\n // the open-WiFi exposure foot-gun (cc ui reads CC_UI_TOKEN as\n // fallback when --token isn't passed).\n \"CC_UI_HOST\" to \"0.0.0.0\",\n \"CC_UI_TOKEN\" to ccUiToken(),\n )\n\n // Layer order: defaults < DNS fix < LLM key envs < caller-provided extras.\n val merged = defaults + dnsFixEnv() + loadLlmKeyEnvs() + extra\n return merged.entries\n .map { (k, v) -> \"$k=$v\" }\n .toTypedArray()\n }\n\n /**\n * Android node DNS fix (module 101 Phase 2). On Android, node's\n * `dns.lookup` (getaddrinfo → netd) fails with ENOTFOUND inside the cc\n * subprocess (the per-uid netId resolver context the framework gives Java\n * networking isn't wired up for a raw forked process), so `fetch`/HTTPS —\n * the LLM call, cookie collectors, update checks — all break. c-ares\n * (`dns.resolve`) over plain UDP DOES work. We ship a tiny `--require`\n * preload that re-points `dns.lookup` at c-ares, and feed it the device's\n * REAL DNS servers (read here via the framework, which works on every\n * phone) so it isn't hostage to a hard-coded public resolver that some\n * networks block. Returns empty (no NODE_OPTIONS) if the preload can't be\n * staged — better a clear \"fetch failed\" than node refusing to start on a\n * missing --require module.\n */\n private fun dnsFixEnv(): Map {\n val path = ensureDnsFix() ?: return emptyMap()\n val servers = deviceDnsServers()\n val env = mutableMapOf(\"NODE_OPTIONS\" to \"--require $path\")\n if (servers.isNotEmpty()) env[\"CC_DNS_SERVERS\"] = servers\n return env\n }\n\n /** Active-network DNS servers (comma-joined). Best-effort; \"\" if unknown. */\n private fun deviceDnsServers(): String = try {\n val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE)\n as? android.net.ConnectivityManager\n val net = cm?.activeNetwork\n val lp = net?.let { cm.getLinkProperties(it) }\n lp?.dnsServers?.mapNotNull { it.hostAddress }?.distinct()?.joinToString(\",\") ?: \"\"\n } catch (e: Exception) {\n Timber.tag(\"PtyEnv\").w(e, \"Failed reading device DNS servers\")\n \"\"\n }\n\n /**\n * Stage the DNS-fix preload at `$PREFIX/etc/dns-fix.js` (idempotent — only\n * rewrites if missing or stale). Returns its absolute path, or null on\n * failure (so the caller omits NODE_OPTIONS rather than point at nothing).\n */\n private fun ensureDnsFix(): String? = try {\n val etc = java.io.File(bootstrapper.prefixDir, \"etc\")\n if (!etc.exists()) etc.mkdirs()\n val file = java.io.File(etc, \"dns-fix.js\")\n if (!file.exists() || file.length().toInt() != DNS_FIX_JS.toByteArray().size) {\n file.writeText(DNS_FIX_JS)\n }\n file.absolutePath.takeIf { file.exists() }\n } catch (e: Exception) {\n Timber.tag(\"PtyEnv\").w(e, \"Failed staging dns-fix.js\")\n null\n }\n\n /**\n * Read LLM API keys from the app's `llm_config_secure` EncryptedSharedPreferences\n * (managed by feature-ai's LLMConfigManager) and project them onto the env-var\n * names cc CLI expects (see packages/cli/src/lib/llm-providers.js apiKeyEnv).\n *\n * Empty values are dropped — better to surface \"API key not set\" from cc\n * than to confuse it with an empty string. Returning an empty map on any\n * read error keeps the terminal bootstrap robust against EncryptedSharedPreferences\n * corruption / first-launch races.\n *\n * Keep this list in sync with:\n * feature-ai/.../data/config/LLMConfig.kt#loadSensitiveFields() ← source-of-truth\n * packages/cli/src/lib/llm-providers.js#apiKeyEnv ← consumer\n */\n private fun loadLlmKeyEnvs(): Map = try {\n val masterKey = MasterKey.Builder(context)\n .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n .build()\n val prefs = EncryptedSharedPreferences.create(\n context,\n \"llm_config_secure\",\n masterKey,\n EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,\n )\n // PrefKey from LLMConfig.kt → env var name from cc CLI's llm-providers.js\n // (apiKeyEnv field). Ernie/ChatGLM/Spark have no cc CLI standard env name;\n // we use sensible fallbacks (ERNIE_API_KEY etc) so cc can pick them up via\n // env-var-by-provider lookup if the provider gets wired in later.\n val mapping = listOf(\n \"openai.apiKey\" to \"OPENAI_API_KEY\",\n \"anthropic.apiKey\" to \"ANTHROPIC_API_KEY\",\n \"deepseek.apiKey\" to \"DEEPSEEK_API_KEY\",\n \"qwen.apiKey\" to \"DASHSCOPE_API_KEY\",\n \"gemini.apiKey\" to \"GEMINI_API_KEY\",\n \"volcengine.apiKey\" to \"VOLCENGINE_API_KEY\",\n \"moonshot.apiKey\" to \"MOONSHOT_API_KEY\",\n \"ernie.apiKey\" to \"ERNIE_API_KEY\",\n \"chatglm.apiKey\" to \"ZHIPU_API_KEY\",\n \"spark.apiKey\" to \"SPARK_API_KEY\",\n )\n mapping.mapNotNull { (prefKey, envName) ->\n val value = prefs.getString(prefKey, \"\") ?: \"\"\n if (value.isNotBlank()) envName to value else null\n }.toMap().also { keys ->\n if (keys.isNotEmpty()) {\n Timber.tag(\"PtyEnv\").i(\n \"Injected ${keys.size} LLM key envs from app config: ${keys.keys}\"\n )\n }\n }\n } catch (e: Exception) {\n Timber.tag(\"PtyEnv\").w(e, \"Failed reading llm_config_secure; cc CLI will need manual env keys\")\n emptyMap()\n }\n\n /**\n * Persistent random token for `cc ui` LAN binding. Stored in plain\n * SharedPreferences (not Encrypted) because:\n * - It's a session-level auth bearer, not a long-lived secret\n * - We don't ship a UI to re-generate it explicitly yet (Phase 6 work)\n * - Same UID isolation as encrypted prefs from a SELinux standpoint\n *\n * Generated on first read (32 hex chars). Persists across app upgrades\n * since SharedPreferences live in /data/data//shared_prefs/.\n */\n private fun ccUiToken(): String {\n val prefs = context.getSharedPreferences(\"local_terminal_ui\", Context.MODE_PRIVATE)\n prefs.getString(\"cc_ui_token\", null)?.let { if (it.isNotBlank()) return it }\n val bytes = ByteArray(16)\n java.security.SecureRandom().nextBytes(bytes)\n val token = bytes.joinToString(\"\") { \"%02x\".format(it) }\n prefs.edit().putString(\"cc_ui_token\", token).apply()\n return token\n }\n\n /**\n * Read-only accessor — for UI surfaces (eg the terminal's \"Open Web UI\"\n * button) that want to construct the full http URL with the token.\n */\n fun ccUiTokenSnapshot(): String = ccUiToken()\n\n /**\n * Convenience for the canonical mksh-interactive invocation:\n * `[$PREFIX/bin/mksh, -l]` — `-l` makes mksh a login shell so it sources\n * `$PREFIX/etc/profile` and our defaults take effect inside the shell.\n */\n fun mkshLoginArgv(): Array {\n val mksh = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n return arrayOf(mksh, \"-l\")\n }\n\n /** Path to the mksh executable symlink in `$PREFIX/bin`. */\n val mkshExecutable: String\n get() = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n\n companion object {\n /**\n * The `--require` preload that fixes node DNS on Android (see\n * [dnsFixEnv]). Re-points `dns.lookup` (used by fetch/net.connect) at\n * c-ares `dns.resolve`, seeded with the device's real DNS from\n * `CC_DNS_SERVERS` plus public fallbacks. Pure JS, ES5-safe (no `$`\n * so it survives the Kotlin raw string), self-contained.\n */\n private val DNS_FIX_JS =\n \"\"\"\n // Android node DNS fix (module 101 Phase 2). getaddrinfo (dns.lookup)\n // fails in the cc subprocess; c-ares (dns.resolve) over UDP works.\n // Re-point dns.lookup at c-ares, seeded with the device's real DNS\n // (CC_DNS_SERVERS) + public fallbacks.\n var dns = require('dns');\n var fromEnv = String(process.env.CC_DNS_SERVERS || '')\n .split(',').map(function (s) { return s.trim(); }).filter(Boolean);\n var fallback = ['223.5.5.5', '119.29.29.29', '1.1.1.1', '8.8.8.8'];\n var seen = {}, servers = [];\n fromEnv.concat(fallback).forEach(function (s) {\n if (s && !seen[s]) { seen[s] = 1; servers.push(s); }\n });\n try { dns.setServers(servers); } catch (e) {}\n var origLookup = dns.lookup;\n dns.lookup = function (hostname, opts, cb) {\n if (typeof opts === 'function') { cb = opts; opts = {}; }\n opts = (typeof opts === 'number') ? { family: opts } : (opts || {});\n var fb = function () { return origLookup(hostname, opts, cb); };\n var v6 = function () {\n dns.resolve6(hostname, function (e, a) {\n if (e || !a || !a.length) return fb();\n cb(null, opts.all ? a.map(function (x) {\n return { address: x, family: 6 };\n }) : a[0], 6);\n });\n };\n if (opts.family === 6) return v6();\n dns.resolve4(hostname, function (e, a) {\n if (e || !a || !a.length) return v6();\n cb(null, opts.all ? a.map(function (x) {\n return { address: x, family: 4 };\n }) : a[0], 4);\n });\n };\n \"\"\".trimIndent()\n }\n}\n"}, "50cce2bc899c0e80c5b11c0a3b9410e467357a28": {"t": 1785754067.6862295, "n": 13315, "h": "8742e6ef50e1dd72", "c": "package com.chainlesschain.android.feature.localterminal\n\nimport android.content.Context\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport timber.log.Timber\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Builds the env-var array passed to [LocalPtyClient.Config.envp] for shells\n * started against the `$PREFIX` layout produced by [LocalFilesystemBootstrapper].\n *\n * Centralising env construction here keeps a single source of truth — the\n * design doc §5 Trap 8 catalogues the PATH / TMPDIR / shebang gotchas; the\n * defaults below sidestep all of them.\n *\n * Phase 2 contract (subject to change in Phase 5 when termux-exec lands):\n *\n * PATH = $PREFIX/bin : /system/bin : /system/xbin\n * HOME = /home\n * TMPDIR = $PREFIX/tmp\n * SHELL = $PREFIX/bin/mksh\n * TERM = xterm-256color (overrideable)\n * LANG = en_US.UTF-8 (overrideable)\n * PREFIX = absolute path to $PREFIX (for scripts that need it)\n * ENV = $PREFIX/etc/profile (mksh loads on -l)\n *\n * Phase 2.5 — also injects LLM API keys from the app's encrypted prefs so\n * `cc ask`, `cc agent` etc work out of the box once the user has set a key\n * in the AI settings UI. See [loadLlmKeyEnvs] for the mapping. Empty keys\n * are skipped (no `KEY=` blank entry).\n *\n * Notable omissions until Phase 5:\n *\n * - **No `LD_PRELOAD=$PREFIX/lib/libtermux-exec.so`** — termux-exec\n * vendoring is deferred to Sub-phase 5.4 (only matters when bundled\n * scripts use shebangs like `#!/usr/bin/env python`).\n */\n@Singleton\nclass PtyEnvironment @Inject constructor(\n @ApplicationContext private val context: Context,\n private val bootstrapper: LocalFilesystemBootstrapper,\n) {\n\n /**\n * Build env-var array suitable for [LocalPtyClient.Config.envp].\n *\n * @param extra additional or overriding env entries. `extra` keys\n * override defaults; e.g. `mapOf(\"TERM\" to \"dumb\")` for\n * tests that don't want xterm escapes.\n * @return Array of `KEY=VALUE` strings.\n */\n fun envp(extra: Map = emptyMap()): Array {\n val prefix = bootstrapper.prefixDir.absolutePath\n val home = bootstrapper.homeDir.absolutePath\n val tmp = bootstrapper.tmpDir.absolutePath\n\n val defaults = mapOf(\n \"PATH\" to \"$prefix/bin:/system/bin:/system/xbin\",\n \"HOME\" to home,\n \"TMPDIR\" to tmp,\n \"SHELL\" to \"$prefix/bin/mksh\",\n \"TERM\" to \"xterm-256color\",\n \"LANG\" to \"en_US.UTF-8\",\n \"PREFIX\" to prefix,\n \"ENV\" to \"$prefix/etc/profile\",\n // Phase 2.5 — Node.js DT_RUNPATH was patched to $ORIGIN, so libnode.so\n // finds libcrypto/libssl/libicu* in the same lib// dir. But\n // libnode itself is exec'd via the `$PREFIX/bin/node` symlink whose\n // parent dir is bin/ — to keep the loader happy across symlink\n // chains, also set LD_LIBRARY_PATH explicitly.\n \"LD_LIBRARY_PATH\" to \"$prefix/lib\",\n // Where global node_modules live — chainlesschain CLI is installed\n // under $PREFIX/lib/node_modules/, so `require()` resolution from\n // the shim binary picks it up.\n \"NODE_PATH\" to \"$prefix/lib/node_modules\",\n // `cc ui` defaults — bind 0.0.0.0 so LAN devices on the same WiFi\n // can hit the management UI without adb forward. Auto-token avoids\n // the open-WiFi exposure foot-gun (cc ui reads CC_UI_TOKEN as\n // fallback when --token isn't passed).\n \"CC_UI_HOST\" to \"0.0.0.0\",\n \"CC_UI_TOKEN\" to ccUiToken(),\n )\n\n // Layer order: defaults < DNS fix < LLM key envs < caller-provided extras.\n val merged = defaults + dnsFixEnv() + loadLlmKeyEnvs() + extra\n return merged.entries\n .map { (k, v) -> \"$k=$v\" }\n .toTypedArray()\n }\n\n /**\n * Android node DNS fix (module 101 Phase 2). On Android, node's\n * `dns.lookup` (getaddrinfo → netd) fails with ENOTFOUND inside the cc\n * subprocess (the per-uid netId resolver context the framework gives Java\n * networking isn't wired up for a raw forked process), so `fetch`/HTTPS —\n * the LLM call, cookie collectors, update checks — all break. c-ares\n * (`dns.resolve`) over plain UDP DOES work. We ship a tiny `--require`\n * preload that re-points `dns.lookup` at c-ares, and feed it the device's\n * REAL DNS servers (read here via the framework, which works on every\n * phone) so it isn't hostage to a hard-coded public resolver that some\n * networks block. Returns empty (no NODE_OPTIONS) if the preload can't be\n * staged — better a clear \"fetch failed\" than node refusing to start on a\n * missing --require module.\n */\n private fun dnsFixEnv(): Map {\n val path = ensureDnsFix() ?: return emptyMap()\n val servers = deviceDnsServers()\n val env = mutableMapOf(\"NODE_OPTIONS\" to \"--require $path\")\n if (servers.isNotEmpty()) env[\"CC_DNS_SERVERS\"] = servers\n return env\n }\n\n /** Active-network DNS servers (comma-joined). Best-effort; \"\" if unknown. */\n private fun deviceDnsServers(): String = try {\n val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE)\n as? android.net.ConnectivityManager\n val net = cm?.activeNetwork\n val lp = net?.let { cm.getLinkProperties(it) }\n lp?.dnsServers?.mapNotNull { it.hostAddress }?.distinct()?.joinToString(\",\") ?: \"\"\n } catch (e: Exception) {\n Timber.tag(\"PtyEnv\").w(e, \"Failed reading device DNS servers\")\n \"\"\n }\n\n /**\n * Stage the DNS-fix preload at `$PREFIX/etc/dns-fix.js` (idempotent — only\n * rewrites if missing or stale). Returns its absolute path, or null on\n * failure (so the caller omits NODE_OPTIONS rather than point at nothing).\n */\n private fun ensureDnsFix(): String? = try {\n val etc = java.io.File(bootstrapper.prefixDir, \"etc\")\n if (!etc.exists()) etc.mkdirs()\n val file = java.io.File(etc, \"dns-fix.js\")\n if (!file.exists() || file.length().toInt() != DNS_FIX_JS.toByteArray().size) {\n file.writeText(DNS_FIX_JS)\n }\n file.absolutePath.takeIf { file.exists() }\n } catch (e: Exception) {\n Timber.tag(\"PtyEnv\").w(e, \"Failed staging dns-fix.js\")\n null\n }\n\n /**\n * Read LLM API keys from the app's `llm_config_secure` EncryptedSharedPreferences\n * (managed by feature-ai's LLMConfigManager) and project them onto the env-var\n * names cc CLI expects (see packages/cli/src/lib/llm-providers.js apiKeyEnv).\n *\n * Empty values are dropped — better to surface \"API key not set\" from cc\n * than to confuse it with an empty string. Returning an empty map on any\n * read error keeps the terminal bootstrap robust against EncryptedSharedPreferences\n * corruption / first-launch races.\n *\n * Keep this list in sync with:\n * feature-ai/.../data/config/LLMConfig.kt#loadSensitiveFields() ← source-of-truth\n * packages/cli/src/lib/llm-providers.js#apiKeyEnv ← consumer\n */\n private fun loadLlmKeyEnvs(): Map = try {\n val masterKey = MasterKey.Builder(context)\n .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n .build()\n val prefs = EncryptedSharedPreferences.create(\n context,\n \"llm_config_secure\",\n masterKey,\n EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,\n )\n // PrefKey from LLMConfig.kt → env var name from cc CLI's llm-providers.js\n // (apiKeyEnv field). Ernie/ChatGLM/Spark have no cc CLI standard env name;\n // we use sensible fallbacks (ERNIE_API_KEY etc) so cc can pick them up via\n // env-var-by-provider lookup if the provider gets wired in later.\n val mapping = listOf(\n \"openai.apiKey\" to \"OPENAI_API_KEY\",\n \"anthropic.apiKey\" to \"ANTHROPIC_API_KEY\",\n \"deepseek.apiKey\" to \"DEEPSEEK_API_KEY\",\n \"qwen.apiKey\" to \"DASHSCOPE_API_KEY\",\n \"gemini.apiKey\" to \"GEMINI_API_KEY\",\n \"volcengine.apiKey\" to \"VOLCENGINE_API_KEY\",\n \"moonshot.apiKey\" to \"MOONSHOT_API_KEY\",\n \"ernie.apiKey\" to \"ERNIE_API_KEY\",\n \"chatglm.apiKey\" to \"ZHIPU_API_KEY\",\n \"spark.apiKey\" to \"SPARK_API_KEY\",\n )\n mapping.mapNotNull { (prefKey, envName) ->\n val value = prefs.getString(prefKey, \"\") ?: \"\"\n if (value.isNotBlank()) envName to value else null\n }.toMap().also { keys ->\n if (keys.isNotEmpty()) {\n Timber.tag(\"PtyEnv\").i(\n \"Injected ${keys.size} LLM key envs from app config: ${keys.keys}\"\n )\n }\n }\n } catch (e: Exception) {\n Timber.tag(\"PtyEnv\").w(e, \"Failed reading llm_config_secure; cc CLI will need manual env keys\")\n emptyMap()\n }\n\n /**\n * Persistent random token for `cc ui` LAN binding. Stored in plain\n * SharedPreferences (not Encrypted) because:\n * - It's a session-level auth bearer, not a long-lived secret\n * - We don't ship a UI to re-generate it explicitly yet (Phase 6 work)\n * - Same UID isolation as encrypted prefs from a SELinux standpoint\n *\n * Generated on first read (32 hex chars). Persists across app upgrades\n * since SharedPreferences live in /data/data//shared_prefs/.\n */\n private fun ccUiToken(): String {\n val prefs = context.getSharedPreferences(\"local_terminal_ui\", Context.MODE_PRIVATE)\n prefs.getString(\"cc_ui_token\", null)?.let { if (it.isNotBlank()) return it }\n val bytes = ByteArray(16)\n java.security.SecureRandom().nextBytes(bytes)\n val token = bytes.joinToString(\"\") { \"%02x\".format(it) }\n prefs.edit().putString(\"cc_ui_token\", token).apply()\n return token\n }\n\n /**\n * Read-only accessor — for UI surfaces (eg the terminal's \"Open Web UI\"\n * button) that want to construct the full http URL with the token.\n */\n fun ccUiTokenSnapshot(): String = ccUiToken()\n\n /**\n * Convenience for the canonical mksh-interactive invocation:\n * `[$PREFIX/bin/mksh, -l]` — `-l` makes mksh a login shell so it sources\n * `$PREFIX/etc/profile` and our defaults take effect inside the shell.\n */\n fun mkshLoginArgv(): Array {\n val mksh = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n return arrayOf(mksh, \"-l\")\n }\n\n /** Path to the mksh executable symlink in `$PREFIX/bin`. */\n val mkshExecutable: String\n get() = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n\n companion object {\n /**\n * The `--require` preload that fixes node DNS on Android (see\n * [dnsFixEnv]). Re-points `dns.lookup` (used by fetch/net.connect) at\n * c-ares `dns.resolve`, seeded with the device's real DNS from\n * `CC_DNS_SERVERS` plus public fallbacks. Pure JS, ES5-safe (no `$`\n * so it survives the Kotlin raw string), self-contained.\n */\n private val DNS_FIX_JS =\n \"\"\"\n // Android node DNS fix (module 101 Phase 2). getaddrinfo (dns.lookup)\n // fails in the cc subprocess; c-ares (dns.resolve) over UDP works.\n // Re-point dns.lookup at c-ares, seeded with the device's real DNS\n // (CC_DNS_SERVERS) + public fallbacks.\n var dns = require('dns');\n var fromEnv = String(process.env.CC_DNS_SERVERS || '')\n .split(',').map(function (s) { return s.trim(); }).filter(Boolean);\n var fallback = ['223.5.5.5', '119.29.29.29', '1.1.1.1', '8.8.8.8'];\n var seen = {}, servers = [];\n fromEnv.concat(fallback).forEach(function (s) {\n if (s && !seen[s]) { seen[s] = 1; servers.push(s); }\n });\n try { dns.setServers(servers); } catch (e) {}\n var origLookup = dns.lookup;\n dns.lookup = function (hostname, opts, cb) {\n if (typeof opts === 'function') { cb = opts; opts = {}; }\n opts = (typeof opts === 'number') ? { family: opts } : (opts || {});\n var fb = function () { return origLookup(hostname, opts, cb); };\n var v6 = function () {\n dns.resolve6(hostname, function (e, a) {\n if (e || !a || !a.length) return fb();\n cb(null, opts.all ? a.map(function (x) {\n return { address: x, family: 6 };\n }) : a[0], 6);\n });\n };\n if (opts.family === 6) return v6();\n dns.resolve4(hostname, function (e, a) {\n if (e || !a || !a.length) return v6();\n cb(null, opts.all ? a.map(function (x) {\n return { address: x, family: 4 };\n }) : a[0], 4);\n });\n };\n \"\"\".trimIndent()\n }\n}\n"}, "3b0b2dc09dceaf104b65bdb7bdc4448fc55e53a5": {"t": 1785754069.1280882, "n": 8488, "h": "9967efcbeb545156", "c": "package com.chainlesschain.android.feature.localterminal\n\nimport android.content.Context\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport timber.log.Timber\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Builds the env-var array passed to [LocalPtyClient.Config.envp] for shells\n * started against the `$PREFIX` layout produced by [LocalFilesystemBootstrapper].\n *\n * Centralising env construction here keeps a single source of truth — the\n * design doc §5 Trap 8 catalogues the PATH / TMPDIR / shebang gotchas; the\n * defaults below sidestep all of them.\n *\n * Phase 2 contract (subject to change in Phase 5 when termux-exec lands):\n *\n * PATH = $PREFIX/bin : /system/bin : /system/xbin\n * HOME = /home\n * TMPDIR = $PREFIX/tmp\n * SHELL = $PREFIX/bin/mksh\n * TERM = xterm-256color (overrideable)\n * LANG = en_US.UTF-8 (overrideable)\n * PREFIX = absolute path to $PREFIX (for scripts that need it)\n * ENV = $PREFIX/etc/profile (mksh loads on -l)\n *\n * Phase 2.5 — also injects LLM API keys from the app's encrypted prefs so\n * `cc ask`, `cc agent` etc work out of the box once the user has set a key\n * in the AI settings UI. See [loadLlmKeyEnvs] for the mapping. Empty keys\n * are skipped (no `KEY=` blank entry).\n *\n * Notable omissions until Phase 5:\n *\n * - **No `LD_PRELOAD=$PREFIX/lib/libtermux-exec.so`** — termux-exec\n * vendoring is deferred to Sub-phase 5.4 (only matters when bundled\n * scripts use shebangs like `#!/usr/bin/env python`).\n */\n@Singleton\nclass PtyEnvironment @Inject constructor(\n @ApplicationContext private val context: Context,\n private val bootstrapper: LocalFilesystemBootstrapper,\n) {\n\n /**\n * Build env-var array suitable for [LocalPtyClient.Config.envp].\n *\n * @param extra additional or overriding env entries. `extra` keys\n * override defaults; e.g. `mapOf(\"TERM\" to \"dumb\")` for\n * tests that don't want xterm escapes.\n * @return Array of `KEY=VALUE` strings.\n */\n fun envp(extra: Map = emptyMap()): Array {\n val prefix = bootstrapper.prefixDir.absolutePath\n val home = bootstrapper.homeDir.absolutePath\n val tmp = bootstrapper.tmpDir.absolutePath\n\n val defaults = mapOf(\n \"PATH\" to \"$prefix/bin:/system/bin:/system/xbin\",\n \"HOME\" to home,\n \"TMPDIR\" to tmp,\n \"SHELL\" to \"$prefix/bin/mksh\",\n \"TERM\" to \"xterm-256color\",\n \"LANG\" to \"en_US.UTF-8\",\n \"PREFIX\" to prefix,\n \"ENV\" to \"$prefix/etc/profile\",\n // Phase 2.5 — Node.js DT_RUNPATH was patched to $ORIGIN, so libnode.so\n // finds libcrypto/libssl/libicu* in the same lib// dir. But\n // libnode itself is exec'd via the `$PREFIX/bin/node` symlink whose\n // parent dir is bin/ — to keep the loader happy across symlink\n // chains, also set LD_LIBRARY_PATH explicitly.\n \"LD_LIBRARY_PATH\" to \"$prefix/lib\",\n // Where global node_modules live — chainlesschain CLI is installed\n // under $PREFIX/lib/node_modules/, so `require()` resolution from\n // the shim binary picks it up.\n \"NODE_PATH\" to \"$prefix/lib/node_modules\",\n // `cc ui` defaults — bind 0.0.0.0 so LAN devices on the same WiFi\n // can hit the management UI without adb forward. Auto-token avoids\n // the open-WiFi exposure foot-gun (cc ui reads CC_UI_TOKEN as\n // fallback when --token isn't passed).\n \"CC_UI_HOST\" to \"0.0.0.0\",\n \"CC_UI_TOKEN\" to ccUiToken(),\n )\n\n // Layer order: defaults < LLM key envs < caller-provided extras.\n val merged = defaults + loadLlmKeyEnvs() + extra\n return merged.entries\n .map { (k, v) -> \"$k=$v\" }\n .toTypedArray()\n }\n\n /**\n * Read LLM API keys from the app's `llm_config_secure` EncryptedSharedPreferences\n * (managed by feature-ai's LLMConfigManager) and project them onto the env-var\n * names cc CLI expects (see packages/cli/src/lib/llm-providers.js apiKeyEnv).\n *\n * Empty values are dropped — better to surface \"API key not set\" from cc\n * than to confuse it with an empty string. Returning an empty map on any\n * read error keeps the terminal bootstrap robust against EncryptedSharedPreferences\n * corruption / first-launch races.\n *\n * Keep this list in sync with:\n * feature-ai/.../data/config/LLMConfig.kt#loadSensitiveFields() ← source-of-truth\n * packages/cli/src/lib/llm-providers.js#apiKeyEnv ← consumer\n */\n private fun loadLlmKeyEnvs(): Map = try {\n val masterKey = MasterKey.Builder(context)\n .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n .build()\n val prefs = EncryptedSharedPreferences.create(\n context,\n \"llm_config_secure\",\n masterKey,\n EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,\n )\n // PrefKey from LLMConfig.kt → env var name from cc CLI's llm-providers.js\n // (apiKeyEnv field). Ernie/ChatGLM/Spark have no cc CLI standard env name;\n // we use sensible fallbacks (ERNIE_API_KEY etc) so cc can pick them up via\n // env-var-by-provider lookup if the provider gets wired in later.\n val mapping = listOf(\n \"openai.apiKey\" to \"OPENAI_API_KEY\",\n \"anthropic.apiKey\" to \"ANTHROPIC_API_KEY\",\n \"deepseek.apiKey\" to \"DEEPSEEK_API_KEY\",\n \"qwen.apiKey\" to \"DASHSCOPE_API_KEY\",\n \"gemini.apiKey\" to \"GEMINI_API_KEY\",\n \"volcengine.apiKey\" to \"VOLCENGINE_API_KEY\",\n \"moonshot.apiKey\" to \"MOONSHOT_API_KEY\",\n \"ernie.apiKey\" to \"ERNIE_API_KEY\",\n \"chatglm.apiKey\" to \"ZHIPU_API_KEY\",\n \"spark.apiKey\" to \"SPARK_API_KEY\",\n )\n mapping.mapNotNull { (prefKey, envName) ->\n val value = prefs.getString(prefKey, \"\") ?: \"\"\n if (value.isNotBlank()) envName to value else null\n }.toMap().also { keys ->\n if (keys.isNotEmpty()) {\n Timber.tag(\"PtyEnv\").i(\n \"Injected ${keys.size} LLM key envs from app config: ${keys.keys}\"\n )\n }\n }\n } catch (e: Exception) {\n Timber.tag(\"PtyEnv\").w(e, \"Failed reading llm_config_secure; cc CLI will need manual env keys\")\n emptyMap()\n }\n\n /**\n * Persistent random token for `cc ui` LAN binding. Stored in plain\n * SharedPreferences (not Encrypted) because:\n * - It's a session-level auth bearer, not a long-lived secret\n * - We don't ship a UI to re-generate it explicitly yet (Phase 6 work)\n * - Same UID isolation as encrypted prefs from a SELinux standpoint\n *\n * Generated on first read (32 hex chars). Persists across app upgrades\n * since SharedPreferences live in /data/data//shared_prefs/.\n */\n private fun ccUiToken(): String {\n val prefs = context.getSharedPreferences(\"local_terminal_ui\", Context.MODE_PRIVATE)\n prefs.getString(\"cc_ui_token\", null)?.let { if (it.isNotBlank()) return it }\n val bytes = ByteArray(16)\n java.security.SecureRandom().nextBytes(bytes)\n val token = bytes.joinToString(\"\") { \"%02x\".format(it) }\n prefs.edit().putString(\"cc_ui_token\", token).apply()\n return token\n }\n\n /**\n * Read-only accessor — for UI surfaces (eg the terminal's \"Open Web UI\"\n * button) that want to construct the full http URL with the token.\n */\n fun ccUiTokenSnapshot(): String = ccUiToken()\n\n /**\n * Convenience for the canonical mksh-interactive invocation:\n * `[$PREFIX/bin/mksh, -l]` — `-l` makes mksh a login shell so it sources\n * `$PREFIX/etc/profile` and our defaults take effect inside the shell.\n */\n fun mkshLoginArgv(): Array {\n val mksh = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n return arrayOf(mksh, \"-l\")\n }\n\n /** Path to the mksh executable symlink in `$PREFIX/bin`. */\n val mkshExecutable: String\n get() = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n}\n"}, "c3fe912fefd60bb6332748f59241fb6de61e9c5b": {"t": 1785754071.9448955, "n": 8488, "h": "9967efcbeb545156", "c": "package com.chainlesschain.android.feature.localterminal\n\nimport android.content.Context\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport timber.log.Timber\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Builds the env-var array passed to [LocalPtyClient.Config.envp] for shells\n * started against the `$PREFIX` layout produced by [LocalFilesystemBootstrapper].\n *\n * Centralising env construction here keeps a single source of truth — the\n * design doc §5 Trap 8 catalogues the PATH / TMPDIR / shebang gotchas; the\n * defaults below sidestep all of them.\n *\n * Phase 2 contract (subject to change in Phase 5 when termux-exec lands):\n *\n * PATH = $PREFIX/bin : /system/bin : /system/xbin\n * HOME = /home\n * TMPDIR = $PREFIX/tmp\n * SHELL = $PREFIX/bin/mksh\n * TERM = xterm-256color (overrideable)\n * LANG = en_US.UTF-8 (overrideable)\n * PREFIX = absolute path to $PREFIX (for scripts that need it)\n * ENV = $PREFIX/etc/profile (mksh loads on -l)\n *\n * Phase 2.5 — also injects LLM API keys from the app's encrypted prefs so\n * `cc ask`, `cc agent` etc work out of the box once the user has set a key\n * in the AI settings UI. See [loadLlmKeyEnvs] for the mapping. Empty keys\n * are skipped (no `KEY=` blank entry).\n *\n * Notable omissions until Phase 5:\n *\n * - **No `LD_PRELOAD=$PREFIX/lib/libtermux-exec.so`** — termux-exec\n * vendoring is deferred to Sub-phase 5.4 (only matters when bundled\n * scripts use shebangs like `#!/usr/bin/env python`).\n */\n@Singleton\nclass PtyEnvironment @Inject constructor(\n @ApplicationContext private val context: Context,\n private val bootstrapper: LocalFilesystemBootstrapper,\n) {\n\n /**\n * Build env-var array suitable for [LocalPtyClient.Config.envp].\n *\n * @param extra additional or overriding env entries. `extra` keys\n * override defaults; e.g. `mapOf(\"TERM\" to \"dumb\")` for\n * tests that don't want xterm escapes.\n * @return Array of `KEY=VALUE` strings.\n */\n fun envp(extra: Map = emptyMap()): Array {\n val prefix = bootstrapper.prefixDir.absolutePath\n val home = bootstrapper.homeDir.absolutePath\n val tmp = bootstrapper.tmpDir.absolutePath\n\n val defaults = mapOf(\n \"PATH\" to \"$prefix/bin:/system/bin:/system/xbin\",\n \"HOME\" to home,\n \"TMPDIR\" to tmp,\n \"SHELL\" to \"$prefix/bin/mksh\",\n \"TERM\" to \"xterm-256color\",\n \"LANG\" to \"en_US.UTF-8\",\n \"PREFIX\" to prefix,\n \"ENV\" to \"$prefix/etc/profile\",\n // Phase 2.5 — Node.js DT_RUNPATH was patched to $ORIGIN, so libnode.so\n // finds libcrypto/libssl/libicu* in the same lib// dir. But\n // libnode itself is exec'd via the `$PREFIX/bin/node` symlink whose\n // parent dir is bin/ — to keep the loader happy across symlink\n // chains, also set LD_LIBRARY_PATH explicitly.\n \"LD_LIBRARY_PATH\" to \"$prefix/lib\",\n // Where global node_modules live — chainlesschain CLI is installed\n // under $PREFIX/lib/node_modules/, so `require()` resolution from\n // the shim binary picks it up.\n \"NODE_PATH\" to \"$prefix/lib/node_modules\",\n // `cc ui` defaults — bind 0.0.0.0 so LAN devices on the same WiFi\n // can hit the management UI without adb forward. Auto-token avoids\n // the open-WiFi exposure foot-gun (cc ui reads CC_UI_TOKEN as\n // fallback when --token isn't passed).\n \"CC_UI_HOST\" to \"0.0.0.0\",\n \"CC_UI_TOKEN\" to ccUiToken(),\n )\n\n // Layer order: defaults < LLM key envs < caller-provided extras.\n val merged = defaults + loadLlmKeyEnvs() + extra\n return merged.entries\n .map { (k, v) -> \"$k=$v\" }\n .toTypedArray()\n }\n\n /**\n * Read LLM API keys from the app's `llm_config_secure` EncryptedSharedPreferences\n * (managed by feature-ai's LLMConfigManager) and project them onto the env-var\n * names cc CLI expects (see packages/cli/src/lib/llm-providers.js apiKeyEnv).\n *\n * Empty values are dropped — better to surface \"API key not set\" from cc\n * than to confuse it with an empty string. Returning an empty map on any\n * read error keeps the terminal bootstrap robust against EncryptedSharedPreferences\n * corruption / first-launch races.\n *\n * Keep this list in sync with:\n * feature-ai/.../data/config/LLMConfig.kt#loadSensitiveFields() ← source-of-truth\n * packages/cli/src/lib/llm-providers.js#apiKeyEnv ← consumer\n */\n private fun loadLlmKeyEnvs(): Map = try {\n val masterKey = MasterKey.Builder(context)\n .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n .build()\n val prefs = EncryptedSharedPreferences.create(\n context,\n \"llm_config_secure\",\n masterKey,\n EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,\n )\n // PrefKey from LLMConfig.kt → env var name from cc CLI's llm-providers.js\n // (apiKeyEnv field). Ernie/ChatGLM/Spark have no cc CLI standard env name;\n // we use sensible fallbacks (ERNIE_API_KEY etc) so cc can pick them up via\n // env-var-by-provider lookup if the provider gets wired in later.\n val mapping = listOf(\n \"openai.apiKey\" to \"OPENAI_API_KEY\",\n \"anthropic.apiKey\" to \"ANTHROPIC_API_KEY\",\n \"deepseek.apiKey\" to \"DEEPSEEK_API_KEY\",\n \"qwen.apiKey\" to \"DASHSCOPE_API_KEY\",\n \"gemini.apiKey\" to \"GEMINI_API_KEY\",\n \"volcengine.apiKey\" to \"VOLCENGINE_API_KEY\",\n \"moonshot.apiKey\" to \"MOONSHOT_API_KEY\",\n \"ernie.apiKey\" to \"ERNIE_API_KEY\",\n \"chatglm.apiKey\" to \"ZHIPU_API_KEY\",\n \"spark.apiKey\" to \"SPARK_API_KEY\",\n )\n mapping.mapNotNull { (prefKey, envName) ->\n val value = prefs.getString(prefKey, \"\") ?: \"\"\n if (value.isNotBlank()) envName to value else null\n }.toMap().also { keys ->\n if (keys.isNotEmpty()) {\n Timber.tag(\"PtyEnv\").i(\n \"Injected ${keys.size} LLM key envs from app config: ${keys.keys}\"\n )\n }\n }\n } catch (e: Exception) {\n Timber.tag(\"PtyEnv\").w(e, \"Failed reading llm_config_secure; cc CLI will need manual env keys\")\n emptyMap()\n }\n\n /**\n * Persistent random token for `cc ui` LAN binding. Stored in plain\n * SharedPreferences (not Encrypted) because:\n * - It's a session-level auth bearer, not a long-lived secret\n * - We don't ship a UI to re-generate it explicitly yet (Phase 6 work)\n * - Same UID isolation as encrypted prefs from a SELinux standpoint\n *\n * Generated on first read (32 hex chars). Persists across app upgrades\n * since SharedPreferences live in /data/data//shared_prefs/.\n */\n private fun ccUiToken(): String {\n val prefs = context.getSharedPreferences(\"local_terminal_ui\", Context.MODE_PRIVATE)\n prefs.getString(\"cc_ui_token\", null)?.let { if (it.isNotBlank()) return it }\n val bytes = ByteArray(16)\n java.security.SecureRandom().nextBytes(bytes)\n val token = bytes.joinToString(\"\") { \"%02x\".format(it) }\n prefs.edit().putString(\"cc_ui_token\", token).apply()\n return token\n }\n\n /**\n * Read-only accessor — for UI surfaces (eg the terminal's \"Open Web UI\"\n * button) that want to construct the full http URL with the token.\n */\n fun ccUiTokenSnapshot(): String = ccUiToken()\n\n /**\n * Convenience for the canonical mksh-interactive invocation:\n * `[$PREFIX/bin/mksh, -l]` — `-l` makes mksh a login shell so it sources\n * `$PREFIX/etc/profile` and our defaults take effect inside the shell.\n */\n fun mkshLoginArgv(): Array {\n val mksh = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n return arrayOf(mksh, \"-l\")\n }\n\n /** Path to the mksh executable symlink in `$PREFIX/bin`. */\n val mkshExecutable: String\n get() = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n}\n"}, "6fe6a5550394d589998486d26bc36bb8d24e9212": {"t": 1785754074.6026175, "n": 8488, "h": "9967efcbeb545156", "c": "package com.chainlesschain.android.feature.localterminal\n\nimport android.content.Context\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport timber.log.Timber\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Builds the env-var array passed to [LocalPtyClient.Config.envp] for shells\n * started against the `$PREFIX` layout produced by [LocalFilesystemBootstrapper].\n *\n * Centralising env construction here keeps a single source of truth — the\n * design doc §5 Trap 8 catalogues the PATH / TMPDIR / shebang gotchas; the\n * defaults below sidestep all of them.\n *\n * Phase 2 contract (subject to change in Phase 5 when termux-exec lands):\n *\n * PATH = $PREFIX/bin : /system/bin : /system/xbin\n * HOME = /home\n * TMPDIR = $PREFIX/tmp\n * SHELL = $PREFIX/bin/mksh\n * TERM = xterm-256color (overrideable)\n * LANG = en_US.UTF-8 (overrideable)\n * PREFIX = absolute path to $PREFIX (for scripts that need it)\n * ENV = $PREFIX/etc/profile (mksh loads on -l)\n *\n * Phase 2.5 — also injects LLM API keys from the app's encrypted prefs so\n * `cc ask`, `cc agent` etc work out of the box once the user has set a key\n * in the AI settings UI. See [loadLlmKeyEnvs] for the mapping. Empty keys\n * are skipped (no `KEY=` blank entry).\n *\n * Notable omissions until Phase 5:\n *\n * - **No `LD_PRELOAD=$PREFIX/lib/libtermux-exec.so`** — termux-exec\n * vendoring is deferred to Sub-phase 5.4 (only matters when bundled\n * scripts use shebangs like `#!/usr/bin/env python`).\n */\n@Singleton\nclass PtyEnvironment @Inject constructor(\n @ApplicationContext private val context: Context,\n private val bootstrapper: LocalFilesystemBootstrapper,\n) {\n\n /**\n * Build env-var array suitable for [LocalPtyClient.Config.envp].\n *\n * @param extra additional or overriding env entries. `extra` keys\n * override defaults; e.g. `mapOf(\"TERM\" to \"dumb\")` for\n * tests that don't want xterm escapes.\n * @return Array of `KEY=VALUE` strings.\n */\n fun envp(extra: Map = emptyMap()): Array {\n val prefix = bootstrapper.prefixDir.absolutePath\n val home = bootstrapper.homeDir.absolutePath\n val tmp = bootstrapper.tmpDir.absolutePath\n\n val defaults = mapOf(\n \"PATH\" to \"$prefix/bin:/system/bin:/system/xbin\",\n \"HOME\" to home,\n \"TMPDIR\" to tmp,\n \"SHELL\" to \"$prefix/bin/mksh\",\n \"TERM\" to \"xterm-256color\",\n \"LANG\" to \"en_US.UTF-8\",\n \"PREFIX\" to prefix,\n \"ENV\" to \"$prefix/etc/profile\",\n // Phase 2.5 — Node.js DT_RUNPATH was patched to $ORIGIN, so libnode.so\n // finds libcrypto/libssl/libicu* in the same lib// dir. But\n // libnode itself is exec'd via the `$PREFIX/bin/node` symlink whose\n // parent dir is bin/ — to keep the loader happy across symlink\n // chains, also set LD_LIBRARY_PATH explicitly.\n \"LD_LIBRARY_PATH\" to \"$prefix/lib\",\n // Where global node_modules live — chainlesschain CLI is installed\n // under $PREFIX/lib/node_modules/, so `require()` resolution from\n // the shim binary picks it up.\n \"NODE_PATH\" to \"$prefix/lib/node_modules\",\n // `cc ui` defaults — bind 0.0.0.0 so LAN devices on the same WiFi\n // can hit the management UI without adb forward. Auto-token avoids\n // the open-WiFi exposure foot-gun (cc ui reads CC_UI_TOKEN as\n // fallback when --token isn't passed).\n \"CC_UI_HOST\" to \"0.0.0.0\",\n \"CC_UI_TOKEN\" to ccUiToken(),\n )\n\n // Layer order: defaults < LLM key envs < caller-provided extras.\n val merged = defaults + loadLlmKeyEnvs() + extra\n return merged.entries\n .map { (k, v) -> \"$k=$v\" }\n .toTypedArray()\n }\n\n /**\n * Read LLM API keys from the app's `llm_config_secure` EncryptedSharedPreferences\n * (managed by feature-ai's LLMConfigManager) and project them onto the env-var\n * names cc CLI expects (see packages/cli/src/lib/llm-providers.js apiKeyEnv).\n *\n * Empty values are dropped — better to surface \"API key not set\" from cc\n * than to confuse it with an empty string. Returning an empty map on any\n * read error keeps the terminal bootstrap robust against EncryptedSharedPreferences\n * corruption / first-launch races.\n *\n * Keep this list in sync with:\n * feature-ai/.../data/config/LLMConfig.kt#loadSensitiveFields() ← source-of-truth\n * packages/cli/src/lib/llm-providers.js#apiKeyEnv ← consumer\n */\n private fun loadLlmKeyEnvs(): Map = try {\n val masterKey = MasterKey.Builder(context)\n .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n .build()\n val prefs = EncryptedSharedPreferences.create(\n context,\n \"llm_config_secure\",\n masterKey,\n EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,\n )\n // PrefKey from LLMConfig.kt → env var name from cc CLI's llm-providers.js\n // (apiKeyEnv field). Ernie/ChatGLM/Spark have no cc CLI standard env name;\n // we use sensible fallbacks (ERNIE_API_KEY etc) so cc can pick them up via\n // env-var-by-provider lookup if the provider gets wired in later.\n val mapping = listOf(\n \"openai.apiKey\" to \"OPENAI_API_KEY\",\n \"anthropic.apiKey\" to \"ANTHROPIC_API_KEY\",\n \"deepseek.apiKey\" to \"DEEPSEEK_API_KEY\",\n \"qwen.apiKey\" to \"DASHSCOPE_API_KEY\",\n \"gemini.apiKey\" to \"GEMINI_API_KEY\",\n \"volcengine.apiKey\" to \"VOLCENGINE_API_KEY\",\n \"moonshot.apiKey\" to \"MOONSHOT_API_KEY\",\n \"ernie.apiKey\" to \"ERNIE_API_KEY\",\n \"chatglm.apiKey\" to \"ZHIPU_API_KEY\",\n \"spark.apiKey\" to \"SPARK_API_KEY\",\n )\n mapping.mapNotNull { (prefKey, envName) ->\n val value = prefs.getString(prefKey, \"\") ?: \"\"\n if (value.isNotBlank()) envName to value else null\n }.toMap().also { keys ->\n if (keys.isNotEmpty()) {\n Timber.tag(\"PtyEnv\").i(\n \"Injected ${keys.size} LLM key envs from app config: ${keys.keys}\"\n )\n }\n }\n } catch (e: Exception) {\n Timber.tag(\"PtyEnv\").w(e, \"Failed reading llm_config_secure; cc CLI will need manual env keys\")\n emptyMap()\n }\n\n /**\n * Persistent random token for `cc ui` LAN binding. Stored in plain\n * SharedPreferences (not Encrypted) because:\n * - It's a session-level auth bearer, not a long-lived secret\n * - We don't ship a UI to re-generate it explicitly yet (Phase 6 work)\n * - Same UID isolation as encrypted prefs from a SELinux standpoint\n *\n * Generated on first read (32 hex chars). Persists across app upgrades\n * since SharedPreferences live in /data/data//shared_prefs/.\n */\n private fun ccUiToken(): String {\n val prefs = context.getSharedPreferences(\"local_terminal_ui\", Context.MODE_PRIVATE)\n prefs.getString(\"cc_ui_token\", null)?.let { if (it.isNotBlank()) return it }\n val bytes = ByteArray(16)\n java.security.SecureRandom().nextBytes(bytes)\n val token = bytes.joinToString(\"\") { \"%02x\".format(it) }\n prefs.edit().putString(\"cc_ui_token\", token).apply()\n return token\n }\n\n /**\n * Read-only accessor — for UI surfaces (eg the terminal's \"Open Web UI\"\n * button) that want to construct the full http URL with the token.\n */\n fun ccUiTokenSnapshot(): String = ccUiToken()\n\n /**\n * Convenience for the canonical mksh-interactive invocation:\n * `[$PREFIX/bin/mksh, -l]` — `-l` makes mksh a login shell so it sources\n * `$PREFIX/etc/profile` and our defaults take effect inside the shell.\n */\n fun mkshLoginArgv(): Array {\n val mksh = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n return arrayOf(mksh, \"-l\")\n }\n\n /** Path to the mksh executable symlink in `$PREFIX/bin`. */\n val mkshExecutable: String\n get() = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n}\n"}, "f5e628a94581d306b794752998416ae709a92a4a": {"t": 1785754075.9513526, "n": 6937, "h": "0b9689c70e41e9b6", "c": "package com.chainlesschain.android.feature.localterminal\n\nimport android.content.Context\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport timber.log.Timber\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Builds the env-var array passed to [LocalPtyClient.Config.envp] for shells\n * started against the `$PREFIX` layout produced by [LocalFilesystemBootstrapper].\n *\n * Centralising env construction here keeps a single source of truth — the\n * design doc §5 Trap 8 catalogues the PATH / TMPDIR / shebang gotchas; the\n * defaults below sidestep all of them.\n *\n * Phase 2 contract (subject to change in Phase 5 when termux-exec lands):\n *\n * PATH = $PREFIX/bin : /system/bin : /system/xbin\n * HOME = /home\n * TMPDIR = $PREFIX/tmp\n * SHELL = $PREFIX/bin/mksh\n * TERM = xterm-256color (overrideable)\n * LANG = en_US.UTF-8 (overrideable)\n * PREFIX = absolute path to $PREFIX (for scripts that need it)\n * ENV = $PREFIX/etc/profile (mksh loads on -l)\n *\n * Phase 2.5 — also injects LLM API keys from the app's encrypted prefs so\n * `cc ask`, `cc agent` etc work out of the box once the user has set a key\n * in the AI settings UI. See [loadLlmKeyEnvs] for the mapping. Empty keys\n * are skipped (no `KEY=` blank entry).\n *\n * Notable omissions until Phase 5:\n *\n * - **No `LD_PRELOAD=$PREFIX/lib/libtermux-exec.so`** — termux-exec\n * vendoring is deferred to Sub-phase 5.4 (only matters when bundled\n * scripts use shebangs like `#!/usr/bin/env python`).\n */\n@Singleton\nclass PtyEnvironment @Inject constructor(\n @ApplicationContext private val context: Context,\n private val bootstrapper: LocalFilesystemBootstrapper,\n) {\n\n /**\n * Build env-var array suitable for [LocalPtyClient.Config.envp].\n *\n * @param extra additional or overriding env entries. `extra` keys\n * override defaults; e.g. `mapOf(\"TERM\" to \"dumb\")` for\n * tests that don't want xterm escapes.\n * @return Array of `KEY=VALUE` strings.\n */\n fun envp(extra: Map = emptyMap()): Array {\n val prefix = bootstrapper.prefixDir.absolutePath\n val home = bootstrapper.homeDir.absolutePath\n val tmp = bootstrapper.tmpDir.absolutePath\n\n val defaults = mapOf(\n \"PATH\" to \"$prefix/bin:/system/bin:/system/xbin\",\n \"HOME\" to home,\n \"TMPDIR\" to tmp,\n \"SHELL\" to \"$prefix/bin/mksh\",\n \"TERM\" to \"xterm-256color\",\n \"LANG\" to \"en_US.UTF-8\",\n \"PREFIX\" to prefix,\n \"ENV\" to \"$prefix/etc/profile\",\n // Phase 2.5 — Node.js DT_RUNPATH was patched to $ORIGIN, so libnode.so\n // finds libcrypto/libssl/libicu* in the same lib// dir. But\n // libnode itself is exec'd via the `$PREFIX/bin/node` symlink whose\n // parent dir is bin/ — to keep the loader happy across symlink\n // chains, also set LD_LIBRARY_PATH explicitly.\n \"LD_LIBRARY_PATH\" to \"$prefix/lib\",\n // Where global node_modules live — chainlesschain CLI is installed\n // under $PREFIX/lib/node_modules/, so `require()` resolution from\n // the shim binary picks it up.\n \"NODE_PATH\" to \"$prefix/lib/node_modules\",\n )\n\n // Layer order: defaults < LLM key envs < caller-provided extras.\n val merged = defaults + loadLlmKeyEnvs() + extra\n return merged.entries\n .map { (k, v) -> \"$k=$v\" }\n .toTypedArray()\n }\n\n /**\n * Read LLM API keys from the app's `llm_config_secure` EncryptedSharedPreferences\n * (managed by feature-ai's LLMConfigManager) and project them onto the env-var\n * names cc CLI expects (see packages/cli/src/lib/llm-providers.js apiKeyEnv).\n *\n * Empty values are dropped — better to surface \"API key not set\" from cc\n * than to confuse it with an empty string. Returning an empty map on any\n * read error keeps the terminal bootstrap robust against EncryptedSharedPreferences\n * corruption / first-launch races.\n *\n * Keep this list in sync with:\n * feature-ai/.../data/config/LLMConfig.kt#loadSensitiveFields() ← source-of-truth\n * packages/cli/src/lib/llm-providers.js#apiKeyEnv ← consumer\n */\n private fun loadLlmKeyEnvs(): Map = try {\n val masterKey = MasterKey.Builder(context)\n .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n .build()\n val prefs = EncryptedSharedPreferences.create(\n context,\n \"llm_config_secure\",\n masterKey,\n EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,\n )\n // PrefKey from LLMConfig.kt → env var name from cc CLI's llm-providers.js\n // (apiKeyEnv field). Ernie/ChatGLM/Spark have no cc CLI standard env name;\n // we use sensible fallbacks (ERNIE_API_KEY etc) so cc can pick them up via\n // env-var-by-provider lookup if the provider gets wired in later.\n val mapping = listOf(\n \"openai.apiKey\" to \"OPENAI_API_KEY\",\n \"anthropic.apiKey\" to \"ANTHROPIC_API_KEY\",\n \"deepseek.apiKey\" to \"DEEPSEEK_API_KEY\",\n \"qwen.apiKey\" to \"DASHSCOPE_API_KEY\",\n \"gemini.apiKey\" to \"GEMINI_API_KEY\",\n \"volcengine.apiKey\" to \"VOLCENGINE_API_KEY\",\n \"moonshot.apiKey\" to \"MOONSHOT_API_KEY\",\n \"ernie.apiKey\" to \"ERNIE_API_KEY\",\n \"chatglm.apiKey\" to \"ZHIPU_API_KEY\",\n \"spark.apiKey\" to \"SPARK_API_KEY\",\n )\n mapping.mapNotNull { (prefKey, envName) ->\n val value = prefs.getString(prefKey, \"\") ?: \"\"\n if (value.isNotBlank()) envName to value else null\n }.toMap().also { keys ->\n if (keys.isNotEmpty()) {\n Timber.tag(\"PtyEnv\").i(\n \"Injected ${keys.size} LLM key envs from app config: ${keys.keys}\"\n )\n }\n }\n } catch (e: Exception) {\n Timber.tag(\"PtyEnv\").w(e, \"Failed reading llm_config_secure; cc CLI will need manual env keys\")\n emptyMap()\n }\n\n /**\n * Convenience for the canonical mksh-interactive invocation:\n * `[$PREFIX/bin/mksh, -l]` — `-l` makes mksh a login shell so it sources\n * `$PREFIX/etc/profile` and our defaults take effect inside the shell.\n */\n fun mkshLoginArgv(): Array {\n val mksh = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n return arrayOf(mksh, \"-l\")\n }\n\n /** Path to the mksh executable symlink in `$PREFIX/bin`. */\n val mkshExecutable: String\n get() = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n}\n"}, "e2612b2f15916b59ffdb3c21a308f1a0e97bb940": {"t": 1785754077.165487, "n": 3706, "h": "1a98c62a96c501e1", "c": "package com.chainlesschain.android.feature.localterminal\n\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Builds the env-var array passed to [LocalPtyClient.Config.envp] for shells\n * started against the `$PREFIX` layout produced by [LocalFilesystemBootstrapper].\n *\n * Centralising env construction here keeps a single source of truth — the\n * design doc §5 Trap 8 catalogues the PATH / TMPDIR / shebang gotchas; the\n * defaults below sidestep all of them.\n *\n * Phase 2 contract (subject to change in Phase 5 when termux-exec lands):\n *\n * PATH = $PREFIX/bin : /system/bin : /system/xbin\n * HOME = /home\n * TMPDIR = $PREFIX/tmp\n * SHELL = $PREFIX/bin/mksh\n * TERM = xterm-256color (overrideable)\n * LANG = en_US.UTF-8 (overrideable)\n * PREFIX = absolute path to $PREFIX (for scripts that need it)\n * ENV = $PREFIX/etc/profile (mksh loads on -l)\n *\n * Notable omissions until Phase 5:\n *\n * - **No `LD_PRELOAD=$PREFIX/lib/libtermux-exec.so`** — termux-exec\n * vendoring is deferred to Sub-phase 5.4 (only matters when bundled\n * scripts use shebangs like `#!/usr/bin/env python`).\n * - **No PATH entry for `node_modules/.bin`** — the chainlesschain CLI\n * snapshot lands in Phase 5 too.\n */\n@Singleton\nclass PtyEnvironment @Inject constructor(\n private val bootstrapper: LocalFilesystemBootstrapper,\n) {\n\n /**\n * Build env-var array suitable for [LocalPtyClient.Config.envp].\n *\n * @param extra additional or overriding env entries. `extra` keys\n * override defaults; e.g. `mapOf(\"TERM\" to \"dumb\")` for\n * tests that don't want xterm escapes.\n * @return Array of `KEY=VALUE` strings.\n */\n fun envp(extra: Map = emptyMap()): Array {\n val prefix = bootstrapper.prefixDir.absolutePath\n val home = bootstrapper.homeDir.absolutePath\n val tmp = bootstrapper.tmpDir.absolutePath\n\n val defaults = mapOf(\n \"PATH\" to \"$prefix/bin:/system/bin:/system/xbin\",\n \"HOME\" to home,\n \"TMPDIR\" to tmp,\n \"SHELL\" to \"$prefix/bin/mksh\",\n \"TERM\" to \"xterm-256color\",\n \"LANG\" to \"en_US.UTF-8\",\n \"PREFIX\" to prefix,\n \"ENV\" to \"$prefix/etc/profile\",\n // Phase 2.5 — Node.js DT_RUNPATH was patched to $ORIGIN, so libnode.so\n // finds libcrypto/libssl/libicu* in the same lib// dir. But\n // libnode itself is exec'd via the `$PREFIX/bin/node` symlink whose\n // parent dir is bin/ — to keep the loader happy across symlink\n // chains, also set LD_LIBRARY_PATH explicitly.\n \"LD_LIBRARY_PATH\" to \"$prefix/lib\",\n // Where global node_modules live — chainlesschain CLI is installed\n // under $PREFIX/lib/node_modules/, so `require()` resolution from\n // the shim binary picks it up.\n \"NODE_PATH\" to \"$prefix/lib/node_modules\",\n )\n\n val merged = defaults + extra\n return merged.entries\n .map { (k, v) -> \"$k=$v\" }\n .toTypedArray()\n }\n\n /**\n * Convenience for the canonical mksh-interactive invocation:\n * `[$PREFIX/bin/mksh, -l]` — `-l` makes mksh a login shell so it sources\n * `$PREFIX/etc/profile` and our defaults take effect inside the shell.\n */\n fun mkshLoginArgv(): Array {\n val mksh = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n return arrayOf(mksh, \"-l\")\n }\n\n /** Path to the mksh executable symlink in `$PREFIX/bin`. */\n val mkshExecutable: String\n get() = \"${bootstrapper.prefixDir.absolutePath}/bin/mksh\"\n}\n"}}, "android-app/feature-ai/src/test/java/com/chainlesschain/android/feature/ai/tools/CcExecServiceTest.kt": {"9e2a3238426499a3de1d228034e66dab91cbfa2c": {"t": 1785745738.7966983, "n": 8610, "h": "56bda6e72bb74f92", "c": "package com.chainlesschain.android.feature.ai.tools\n\nimport com.chainlesschain.android.feature.localterminal.LocalFilesystemBootstrapper\nimport com.chainlesschain.android.feature.localterminal.PtyEnvironment\nimport io.mockk.every\nimport io.mockk.mockk\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Before\nimport org.junit.Test\nimport java.io.File\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFalse\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertTrue\n\n@OptIn(ExperimentalCoroutinesApi::class)\nclass CcExecServiceTest {\n\n private lateinit var bootstrapper: LocalFilesystemBootstrapper\n private lateinit var ptyEnvironment: PtyEnvironment\n private lateinit var service: CcExecService\n private lateinit var tmpPrefix: File\n private lateinit var tmpHome: File\n\n @Before\n fun setUp() {\n tmpPrefix = File(System.getProperty(\"java.io.tmpdir\"), \"cc-test-prefix-${System.nanoTime()}\")\n tmpPrefix.mkdirs()\n tmpHome = File(tmpPrefix, \"home\").apply { mkdirs() }\n bootstrapper = mockk(relaxed = true)\n every { bootstrapper.prefixDir } returns tmpPrefix\n every { bootstrapper.homeDir } returns tmpHome\n\n ptyEnvironment = mockk(relaxed = true)\n every { ptyEnvironment.envp() } returns arrayOf(\n \"PATH=/usr/bin:/bin\",\n \"HOME=${tmpHome.absolutePath}\",\n \"OPENAI_API_KEY=sk-leak-this-secret\",\n \"ANTHROPIC_API_KEY=anth-leak\",\n \"DEEPSEEK_API_KEY=dsk-leak\",\n \"CC_UI_TOKEN=ui-leak\",\n \"DASHSCOPE_API_KEY=qwen-leak\",\n \"VOLCENGINE_API_KEY=doubao-leak\",\n \"LANG=en_US.UTF-8\",\n \"MY_HARMLESS=value-with=equals=in=it\",\n \"EMPTY=\",\n \"=should-be-skipped-no-key\",\n )\n\n service = CcExecService(bootstrapper, ptyEnvironment)\n }\n\n // ---------- argv ----------\n\n @Test fun `buildArgv composes node ccJs command subargs`() {\n val argv = service.buildArgv(\"/n/node\", \"/cc.js\", \"note\", listOf(\"list\", \"--limit\", \"10\"))\n assertEquals(listOf(\"/n/node\", \"/cc.js\", \"note\", \"list\", \"--limit\", \"10\"), argv)\n }\n\n @Test fun `buildArgv with empty subargs`() {\n val argv = service.buildArgv(\"/n/node\", \"/cc.js\", \"status\", emptyList())\n assertEquals(listOf(\"/n/node\", \"/cc.js\", \"status\"), argv)\n }\n\n // ---------- env filter ----------\n\n @Test fun `buildFilteredEnv strips secret-prefix vars`() {\n val env = service.buildFilteredEnv()\n for (prefix in CcExecService.FORBIDDEN_ENV_PREFIXES) {\n assertTrue(\n env.keys.none { it.startsWith(prefix) },\n \"should have stripped $prefix; got keys: ${env.keys}\"\n )\n }\n }\n\n @Test fun `buildFilteredEnv preserves benign vars`() {\n val env = service.buildFilteredEnv()\n assertEquals(\"/usr/bin:/bin\", env[\"PATH\"])\n assertEquals(\"en_US.UTF-8\", env[\"LANG\"])\n assertEquals(tmpHome.absolutePath, env[\"HOME\"])\n }\n\n @Test fun `buildFilteredEnv preserves vars with = in value`() {\n val env = service.buildFilteredEnv()\n // indexOf('=') uses FIRST '=' as separator → value can contain =\n assertEquals(\"value-with=equals=in=it\", env[\"MY_HARMLESS\"])\n }\n\n @Test fun `buildFilteredEnv keeps empty values (cc CLI distinguishes set-empty vs unset)`() {\n val env = service.buildFilteredEnv()\n // EMPTY= → \"\" preserved (idx=5, k=\"EMPTY\", v=\"\")\n assertTrue(env.containsKey(\"EMPTY\"))\n assertEquals(\"\", env[\"EMPTY\"])\n }\n\n @Test fun `buildFilteredEnv skips malformed entries with no key`() {\n val env = service.buildFilteredEnv()\n // \"=should-be-skipped-no-key\" → idx=0 → idx <= 0 skip\n assertFalse(env.containsValue(\"should-be-skipped-no-key\"))\n }\n\n // ---------- truncation ----------\n\n @Test fun `decodeAndTruncate under limit returns full UTF8`() {\n val bytes = \"hello 你好\".toByteArray(Charsets.UTF_8)\n val out = service.decodeAndTruncate(bytes, 100)\n assertEquals(\"hello 你好\", out)\n }\n\n @Test fun `decodeAndTruncate over limit adds marker`() {\n val bytes = ByteArray(2000) { it.toByte() }\n val out = service.decodeAndTruncate(bytes, 100)\n assertTrue(out.contains(\"TRUNCATED\"))\n assertTrue(out.contains(\"total=2000\"))\n }\n\n @Test fun `decodeAndTruncate empty input`() {\n val out = service.decodeAndTruncate(ByteArray(0), 100)\n assertEquals(\"\", out)\n }\n\n // ---------- run() — missing binaries return clean Error ----------\n\n @Test fun `run returns Error when node binary missing`() = runTest {\n // tmpPrefix/bin/node does NOT exist\n val r = service.run(\"status\", emptyList())\n assertTrue(r is CcResult.Error)\n val err = r as CcResult.Error\n assertTrue(err.reason.contains(\"Node binary missing\"))\n assertNotNull(err.expectedPath)\n assertTrue(err.expectedPath!!.endsWith(\"/bin/node\"))\n }\n\n @Test fun `run returns Error when ccJs missing (node exists)`() = runTest {\n // create stub node binary, not ccJs\n File(tmpPrefix, \"bin\").mkdirs()\n File(tmpPrefix, \"bin/node\").apply {\n createNewFile()\n setExecutable(true)\n }\n val r = service.run(\"status\", emptyList())\n assertTrue(r is CcResult.Error)\n val err = r as CcResult.Error\n assertTrue(err.reason.contains(\"cc CLI snapshot missing\"))\n }\n\n // ---------- B28 fix — async stream drain prevents pipe-buffer deadlock ----------\n //\n // We can't run real `node + cc CLI` in jvmTest (no Termux binary). But we\n // CAN verify the drain path with a host-shell that emits >64KB stdout —\n // that's what would have deadlocked the pre-fix code. On Windows host we\n // skip (uses cmd.exe quoting differences); on linux/mac CI it runs.\n\n @Test fun `B28 — executeArgv drains large stdout without deadlock`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"yes hello | head -c 200000\")\n // Only run if /bin/sh exists (linux/mac); Windows test JVM lacks it.\n if (!File(\"/bin/sh\").exists()) return@runTest\n\n val r = service.executeArgv(\n argv = sh,\n env = mapOf(\"PATH\" to \"/usr/bin:/bin\"),\n cwd = tmpHome,\n timeoutMs = 10_000L,\n )\n assertTrue(r is CcResult.Ok, \"expected Ok; got $r\")\n val ok = r as CcResult.Ok\n assertEquals(0, ok.exitCode)\n // 200KB stdout consumed; result truncated to STDOUT_TRUNCATE_BYTES (4KB)\n assertTrue(ok.stdout.contains(\"TRUNCATED\"))\n assertTrue(ok.stdout.contains(\"total=200000\"))\n }\n\n @Test fun `executeArgv reports non-zero exit`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"exit 42\")\n if (!File(\"/bin/sh\").exists()) return@runTest\n val r = service.executeArgv(sh, mapOf(\"PATH\" to \"/usr/bin:/bin\"), tmpHome, 5_000L)\n assertTrue(r is CcResult.Ok)\n assertEquals(42, (r as CcResult.Ok).exitCode)\n }\n\n @Test fun `executeArgv reports stderr separately`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"echo good; echo BAD >&2; exit 0\")\n if (!File(\"/bin/sh\").exists()) return@runTest\n val r = service.executeArgv(sh, mapOf(\"PATH\" to \"/usr/bin:/bin\"), tmpHome, 5_000L)\n assertTrue(r is CcResult.Ok)\n val ok = r as CcResult.Ok\n assertTrue(ok.stdout.contains(\"good\"))\n assertTrue(ok.stderr.contains(\"BAD\"))\n }\n\n @Test fun `executeArgv timeout kills runaway`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"sleep 30\")\n if (!File(\"/bin/sh\").exists()) return@runTest\n val started = System.currentTimeMillis()\n val r = service.executeArgv(sh, mapOf(\"PATH\" to \"/usr/bin:/bin\"), tmpHome, 500L)\n val elapsed = System.currentTimeMillis() - started\n assertTrue(r is CcResult.Error)\n assertTrue((r as CcResult.Error).reason.contains(\"timeout\"))\n // confirm we didn't actually wait the full sleep (process killed)\n assertTrue(elapsed < 5_000L, \"timeout took too long: ${elapsed}ms\")\n }\n\n @Test fun `executeArgv ProcessBuilder failure returns Error`() = runTest {\n val r = service.executeArgv(\n argv = listOf(\"/this/path/definitely/does/not/exist\"),\n env = mapOf(\"PATH\" to \"/usr/bin\"),\n cwd = tmpHome,\n timeoutMs = 1_000L,\n )\n assertTrue(r is CcResult.Error)\n assertTrue((r as CcResult.Error).reason.contains(\"ProcessBuilder\"))\n }\n}\n"}, "3b0b2dc09dceaf104b65bdb7bdc4448fc55e53a5": {"t": 1785754069.5982516, "n": 8610, "h": "56bda6e72bb74f92", "c": "package com.chainlesschain.android.feature.ai.tools\n\nimport com.chainlesschain.android.feature.localterminal.LocalFilesystemBootstrapper\nimport com.chainlesschain.android.feature.localterminal.PtyEnvironment\nimport io.mockk.every\nimport io.mockk.mockk\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Before\nimport org.junit.Test\nimport java.io.File\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFalse\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertTrue\n\n@OptIn(ExperimentalCoroutinesApi::class)\nclass CcExecServiceTest {\n\n private lateinit var bootstrapper: LocalFilesystemBootstrapper\n private lateinit var ptyEnvironment: PtyEnvironment\n private lateinit var service: CcExecService\n private lateinit var tmpPrefix: File\n private lateinit var tmpHome: File\n\n @Before\n fun setUp() {\n tmpPrefix = File(System.getProperty(\"java.io.tmpdir\"), \"cc-test-prefix-${System.nanoTime()}\")\n tmpPrefix.mkdirs()\n tmpHome = File(tmpPrefix, \"home\").apply { mkdirs() }\n bootstrapper = mockk(relaxed = true)\n every { bootstrapper.prefixDir } returns tmpPrefix\n every { bootstrapper.homeDir } returns tmpHome\n\n ptyEnvironment = mockk(relaxed = true)\n every { ptyEnvironment.envp() } returns arrayOf(\n \"PATH=/usr/bin:/bin\",\n \"HOME=${tmpHome.absolutePath}\",\n \"OPENAI_API_KEY=sk-leak-this-secret\",\n \"ANTHROPIC_API_KEY=anth-leak\",\n \"DEEPSEEK_API_KEY=dsk-leak\",\n \"CC_UI_TOKEN=ui-leak\",\n \"DASHSCOPE_API_KEY=qwen-leak\",\n \"VOLCENGINE_API_KEY=doubao-leak\",\n \"LANG=en_US.UTF-8\",\n \"MY_HARMLESS=value-with=equals=in=it\",\n \"EMPTY=\",\n \"=should-be-skipped-no-key\",\n )\n\n service = CcExecService(bootstrapper, ptyEnvironment)\n }\n\n // ---------- argv ----------\n\n @Test fun `buildArgv composes node ccJs command subargs`() {\n val argv = service.buildArgv(\"/n/node\", \"/cc.js\", \"note\", listOf(\"list\", \"--limit\", \"10\"))\n assertEquals(listOf(\"/n/node\", \"/cc.js\", \"note\", \"list\", \"--limit\", \"10\"), argv)\n }\n\n @Test fun `buildArgv with empty subargs`() {\n val argv = service.buildArgv(\"/n/node\", \"/cc.js\", \"status\", emptyList())\n assertEquals(listOf(\"/n/node\", \"/cc.js\", \"status\"), argv)\n }\n\n // ---------- env filter ----------\n\n @Test fun `buildFilteredEnv strips secret-prefix vars`() {\n val env = service.buildFilteredEnv()\n for (prefix in CcExecService.FORBIDDEN_ENV_PREFIXES) {\n assertTrue(\n env.keys.none { it.startsWith(prefix) },\n \"should have stripped $prefix; got keys: ${env.keys}\"\n )\n }\n }\n\n @Test fun `buildFilteredEnv preserves benign vars`() {\n val env = service.buildFilteredEnv()\n assertEquals(\"/usr/bin:/bin\", env[\"PATH\"])\n assertEquals(\"en_US.UTF-8\", env[\"LANG\"])\n assertEquals(tmpHome.absolutePath, env[\"HOME\"])\n }\n\n @Test fun `buildFilteredEnv preserves vars with = in value`() {\n val env = service.buildFilteredEnv()\n // indexOf('=') uses FIRST '=' as separator → value can contain =\n assertEquals(\"value-with=equals=in=it\", env[\"MY_HARMLESS\"])\n }\n\n @Test fun `buildFilteredEnv keeps empty values (cc CLI distinguishes set-empty vs unset)`() {\n val env = service.buildFilteredEnv()\n // EMPTY= → \"\" preserved (idx=5, k=\"EMPTY\", v=\"\")\n assertTrue(env.containsKey(\"EMPTY\"))\n assertEquals(\"\", env[\"EMPTY\"])\n }\n\n @Test fun `buildFilteredEnv skips malformed entries with no key`() {\n val env = service.buildFilteredEnv()\n // \"=should-be-skipped-no-key\" → idx=0 → idx <= 0 skip\n assertFalse(env.containsValue(\"should-be-skipped-no-key\"))\n }\n\n // ---------- truncation ----------\n\n @Test fun `decodeAndTruncate under limit returns full UTF8`() {\n val bytes = \"hello 你好\".toByteArray(Charsets.UTF_8)\n val out = service.decodeAndTruncate(bytes, 100)\n assertEquals(\"hello 你好\", out)\n }\n\n @Test fun `decodeAndTruncate over limit adds marker`() {\n val bytes = ByteArray(2000) { it.toByte() }\n val out = service.decodeAndTruncate(bytes, 100)\n assertTrue(out.contains(\"TRUNCATED\"))\n assertTrue(out.contains(\"total=2000\"))\n }\n\n @Test fun `decodeAndTruncate empty input`() {\n val out = service.decodeAndTruncate(ByteArray(0), 100)\n assertEquals(\"\", out)\n }\n\n // ---------- run() — missing binaries return clean Error ----------\n\n @Test fun `run returns Error when node binary missing`() = runTest {\n // tmpPrefix/bin/node does NOT exist\n val r = service.run(\"status\", emptyList())\n assertTrue(r is CcResult.Error)\n val err = r as CcResult.Error\n assertTrue(err.reason.contains(\"Node binary missing\"))\n assertNotNull(err.expectedPath)\n assertTrue(err.expectedPath!!.endsWith(\"/bin/node\"))\n }\n\n @Test fun `run returns Error when ccJs missing (node exists)`() = runTest {\n // create stub node binary, not ccJs\n File(tmpPrefix, \"bin\").mkdirs()\n File(tmpPrefix, \"bin/node\").apply {\n createNewFile()\n setExecutable(true)\n }\n val r = service.run(\"status\", emptyList())\n assertTrue(r is CcResult.Error)\n val err = r as CcResult.Error\n assertTrue(err.reason.contains(\"cc CLI snapshot missing\"))\n }\n\n // ---------- B28 fix — async stream drain prevents pipe-buffer deadlock ----------\n //\n // We can't run real `node + cc CLI` in jvmTest (no Termux binary). But we\n // CAN verify the drain path with a host-shell that emits >64KB stdout —\n // that's what would have deadlocked the pre-fix code. On Windows host we\n // skip (uses cmd.exe quoting differences); on linux/mac CI it runs.\n\n @Test fun `B28 — executeArgv drains large stdout without deadlock`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"yes hello | head -c 200000\")\n // Only run if /bin/sh exists (linux/mac); Windows test JVM lacks it.\n if (!File(\"/bin/sh\").exists()) return@runTest\n\n val r = service.executeArgv(\n argv = sh,\n env = mapOf(\"PATH\" to \"/usr/bin:/bin\"),\n cwd = tmpHome,\n timeoutMs = 10_000L,\n )\n assertTrue(r is CcResult.Ok, \"expected Ok; got $r\")\n val ok = r as CcResult.Ok\n assertEquals(0, ok.exitCode)\n // 200KB stdout consumed; result truncated to STDOUT_TRUNCATE_BYTES (4KB)\n assertTrue(ok.stdout.contains(\"TRUNCATED\"))\n assertTrue(ok.stdout.contains(\"total=200000\"))\n }\n\n @Test fun `executeArgv reports non-zero exit`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"exit 42\")\n if (!File(\"/bin/sh\").exists()) return@runTest\n val r = service.executeArgv(sh, mapOf(\"PATH\" to \"/usr/bin:/bin\"), tmpHome, 5_000L)\n assertTrue(r is CcResult.Ok)\n assertEquals(42, (r as CcResult.Ok).exitCode)\n }\n\n @Test fun `executeArgv reports stderr separately`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"echo good; echo BAD >&2; exit 0\")\n if (!File(\"/bin/sh\").exists()) return@runTest\n val r = service.executeArgv(sh, mapOf(\"PATH\" to \"/usr/bin:/bin\"), tmpHome, 5_000L)\n assertTrue(r is CcResult.Ok)\n val ok = r as CcResult.Ok\n assertTrue(ok.stdout.contains(\"good\"))\n assertTrue(ok.stderr.contains(\"BAD\"))\n }\n\n @Test fun `executeArgv timeout kills runaway`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"sleep 30\")\n if (!File(\"/bin/sh\").exists()) return@runTest\n val started = System.currentTimeMillis()\n val r = service.executeArgv(sh, mapOf(\"PATH\" to \"/usr/bin:/bin\"), tmpHome, 500L)\n val elapsed = System.currentTimeMillis() - started\n assertTrue(r is CcResult.Error)\n assertTrue((r as CcResult.Error).reason.contains(\"timeout\"))\n // confirm we didn't actually wait the full sleep (process killed)\n assertTrue(elapsed < 5_000L, \"timeout took too long: ${elapsed}ms\")\n }\n\n @Test fun `executeArgv ProcessBuilder failure returns Error`() = runTest {\n val r = service.executeArgv(\n argv = listOf(\"/this/path/definitely/does/not/exist\"),\n env = mapOf(\"PATH\" to \"/usr/bin\"),\n cwd = tmpHome,\n timeoutMs = 1_000L,\n )\n assertTrue(r is CcResult.Error)\n assertTrue((r as CcResult.Error).reason.contains(\"ProcessBuilder\"))\n }\n}\n"}, "c3fe912fefd60bb6332748f59241fb6de61e9c5b": {"t": 1785754072.407799, "n": 8610, "h": "56bda6e72bb74f92", "c": "package com.chainlesschain.android.feature.ai.tools\n\nimport com.chainlesschain.android.feature.localterminal.LocalFilesystemBootstrapper\nimport com.chainlesschain.android.feature.localterminal.PtyEnvironment\nimport io.mockk.every\nimport io.mockk.mockk\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Before\nimport org.junit.Test\nimport java.io.File\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFalse\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertTrue\n\n@OptIn(ExperimentalCoroutinesApi::class)\nclass CcExecServiceTest {\n\n private lateinit var bootstrapper: LocalFilesystemBootstrapper\n private lateinit var ptyEnvironment: PtyEnvironment\n private lateinit var service: CcExecService\n private lateinit var tmpPrefix: File\n private lateinit var tmpHome: File\n\n @Before\n fun setUp() {\n tmpPrefix = File(System.getProperty(\"java.io.tmpdir\"), \"cc-test-prefix-${System.nanoTime()}\")\n tmpPrefix.mkdirs()\n tmpHome = File(tmpPrefix, \"home\").apply { mkdirs() }\n bootstrapper = mockk(relaxed = true)\n every { bootstrapper.prefixDir } returns tmpPrefix\n every { bootstrapper.homeDir } returns tmpHome\n\n ptyEnvironment = mockk(relaxed = true)\n every { ptyEnvironment.envp() } returns arrayOf(\n \"PATH=/usr/bin:/bin\",\n \"HOME=${tmpHome.absolutePath}\",\n \"OPENAI_API_KEY=sk-leak-this-secret\",\n \"ANTHROPIC_API_KEY=anth-leak\",\n \"DEEPSEEK_API_KEY=dsk-leak\",\n \"CC_UI_TOKEN=ui-leak\",\n \"DASHSCOPE_API_KEY=qwen-leak\",\n \"VOLCENGINE_API_KEY=doubao-leak\",\n \"LANG=en_US.UTF-8\",\n \"MY_HARMLESS=value-with=equals=in=it\",\n \"EMPTY=\",\n \"=should-be-skipped-no-key\",\n )\n\n service = CcExecService(bootstrapper, ptyEnvironment)\n }\n\n // ---------- argv ----------\n\n @Test fun `buildArgv composes node ccJs command subargs`() {\n val argv = service.buildArgv(\"/n/node\", \"/cc.js\", \"note\", listOf(\"list\", \"--limit\", \"10\"))\n assertEquals(listOf(\"/n/node\", \"/cc.js\", \"note\", \"list\", \"--limit\", \"10\"), argv)\n }\n\n @Test fun `buildArgv with empty subargs`() {\n val argv = service.buildArgv(\"/n/node\", \"/cc.js\", \"status\", emptyList())\n assertEquals(listOf(\"/n/node\", \"/cc.js\", \"status\"), argv)\n }\n\n // ---------- env filter ----------\n\n @Test fun `buildFilteredEnv strips secret-prefix vars`() {\n val env = service.buildFilteredEnv()\n for (prefix in CcExecService.FORBIDDEN_ENV_PREFIXES) {\n assertTrue(\n env.keys.none { it.startsWith(prefix) },\n \"should have stripped $prefix; got keys: ${env.keys}\"\n )\n }\n }\n\n @Test fun `buildFilteredEnv preserves benign vars`() {\n val env = service.buildFilteredEnv()\n assertEquals(\"/usr/bin:/bin\", env[\"PATH\"])\n assertEquals(\"en_US.UTF-8\", env[\"LANG\"])\n assertEquals(tmpHome.absolutePath, env[\"HOME\"])\n }\n\n @Test fun `buildFilteredEnv preserves vars with = in value`() {\n val env = service.buildFilteredEnv()\n // indexOf('=') uses FIRST '=' as separator → value can contain =\n assertEquals(\"value-with=equals=in=it\", env[\"MY_HARMLESS\"])\n }\n\n @Test fun `buildFilteredEnv keeps empty values (cc CLI distinguishes set-empty vs unset)`() {\n val env = service.buildFilteredEnv()\n // EMPTY= → \"\" preserved (idx=5, k=\"EMPTY\", v=\"\")\n assertTrue(env.containsKey(\"EMPTY\"))\n assertEquals(\"\", env[\"EMPTY\"])\n }\n\n @Test fun `buildFilteredEnv skips malformed entries with no key`() {\n val env = service.buildFilteredEnv()\n // \"=should-be-skipped-no-key\" → idx=0 → idx <= 0 skip\n assertFalse(env.containsValue(\"should-be-skipped-no-key\"))\n }\n\n // ---------- truncation ----------\n\n @Test fun `decodeAndTruncate under limit returns full UTF8`() {\n val bytes = \"hello 你好\".toByteArray(Charsets.UTF_8)\n val out = service.decodeAndTruncate(bytes, 100)\n assertEquals(\"hello 你好\", out)\n }\n\n @Test fun `decodeAndTruncate over limit adds marker`() {\n val bytes = ByteArray(2000) { it.toByte() }\n val out = service.decodeAndTruncate(bytes, 100)\n assertTrue(out.contains(\"TRUNCATED\"))\n assertTrue(out.contains(\"total=2000\"))\n }\n\n @Test fun `decodeAndTruncate empty input`() {\n val out = service.decodeAndTruncate(ByteArray(0), 100)\n assertEquals(\"\", out)\n }\n\n // ---------- run() — missing binaries return clean Error ----------\n\n @Test fun `run returns Error when node binary missing`() = runTest {\n // tmpPrefix/bin/node does NOT exist\n val r = service.run(\"status\", emptyList())\n assertTrue(r is CcResult.Error)\n val err = r as CcResult.Error\n assertTrue(err.reason.contains(\"Node binary missing\"))\n assertNotNull(err.expectedPath)\n assertTrue(err.expectedPath!!.endsWith(\"/bin/node\"))\n }\n\n @Test fun `run returns Error when ccJs missing (node exists)`() = runTest {\n // create stub node binary, not ccJs\n File(tmpPrefix, \"bin\").mkdirs()\n File(tmpPrefix, \"bin/node\").apply {\n createNewFile()\n setExecutable(true)\n }\n val r = service.run(\"status\", emptyList())\n assertTrue(r is CcResult.Error)\n val err = r as CcResult.Error\n assertTrue(err.reason.contains(\"cc CLI snapshot missing\"))\n }\n\n // ---------- B28 fix — async stream drain prevents pipe-buffer deadlock ----------\n //\n // We can't run real `node + cc CLI` in jvmTest (no Termux binary). But we\n // CAN verify the drain path with a host-shell that emits >64KB stdout —\n // that's what would have deadlocked the pre-fix code. On Windows host we\n // skip (uses cmd.exe quoting differences); on linux/mac CI it runs.\n\n @Test fun `B28 — executeArgv drains large stdout without deadlock`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"yes hello | head -c 200000\")\n // Only run if /bin/sh exists (linux/mac); Windows test JVM lacks it.\n if (!File(\"/bin/sh\").exists()) return@runTest\n\n val r = service.executeArgv(\n argv = sh,\n env = mapOf(\"PATH\" to \"/usr/bin:/bin\"),\n cwd = tmpHome,\n timeoutMs = 10_000L,\n )\n assertTrue(r is CcResult.Ok, \"expected Ok; got $r\")\n val ok = r as CcResult.Ok\n assertEquals(0, ok.exitCode)\n // 200KB stdout consumed; result truncated to STDOUT_TRUNCATE_BYTES (4KB)\n assertTrue(ok.stdout.contains(\"TRUNCATED\"))\n assertTrue(ok.stdout.contains(\"total=200000\"))\n }\n\n @Test fun `executeArgv reports non-zero exit`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"exit 42\")\n if (!File(\"/bin/sh\").exists()) return@runTest\n val r = service.executeArgv(sh, mapOf(\"PATH\" to \"/usr/bin:/bin\"), tmpHome, 5_000L)\n assertTrue(r is CcResult.Ok)\n assertEquals(42, (r as CcResult.Ok).exitCode)\n }\n\n @Test fun `executeArgv reports stderr separately`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"echo good; echo BAD >&2; exit 0\")\n if (!File(\"/bin/sh\").exists()) return@runTest\n val r = service.executeArgv(sh, mapOf(\"PATH\" to \"/usr/bin:/bin\"), tmpHome, 5_000L)\n assertTrue(r is CcResult.Ok)\n val ok = r as CcResult.Ok\n assertTrue(ok.stdout.contains(\"good\"))\n assertTrue(ok.stderr.contains(\"BAD\"))\n }\n\n @Test fun `executeArgv timeout kills runaway`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"sleep 30\")\n if (!File(\"/bin/sh\").exists()) return@runTest\n val started = System.currentTimeMillis()\n val r = service.executeArgv(sh, mapOf(\"PATH\" to \"/usr/bin:/bin\"), tmpHome, 500L)\n val elapsed = System.currentTimeMillis() - started\n assertTrue(r is CcResult.Error)\n assertTrue((r as CcResult.Error).reason.contains(\"timeout\"))\n // confirm we didn't actually wait the full sleep (process killed)\n assertTrue(elapsed < 5_000L, \"timeout took too long: ${elapsed}ms\")\n }\n\n @Test fun `executeArgv ProcessBuilder failure returns Error`() = runTest {\n val r = service.executeArgv(\n argv = listOf(\"/this/path/definitely/does/not/exist\"),\n env = mapOf(\"PATH\" to \"/usr/bin\"),\n cwd = tmpHome,\n timeoutMs = 1_000L,\n )\n assertTrue(r is CcResult.Error)\n assertTrue((r as CcResult.Error).reason.contains(\"ProcessBuilder\"))\n }\n}\n"}, "1c1def6f0ef78d277fa48501cd9a42566dd7082f": {"t": 1785754074.919743, "n": 8610, "h": "56bda6e72bb74f92", "c": "package com.chainlesschain.android.feature.ai.tools\n\nimport com.chainlesschain.android.feature.localterminal.LocalFilesystemBootstrapper\nimport com.chainlesschain.android.feature.localterminal.PtyEnvironment\nimport io.mockk.every\nimport io.mockk.mockk\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Before\nimport org.junit.Test\nimport java.io.File\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFalse\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertTrue\n\n@OptIn(ExperimentalCoroutinesApi::class)\nclass CcExecServiceTest {\n\n private lateinit var bootstrapper: LocalFilesystemBootstrapper\n private lateinit var ptyEnvironment: PtyEnvironment\n private lateinit var service: CcExecService\n private lateinit var tmpPrefix: File\n private lateinit var tmpHome: File\n\n @Before\n fun setUp() {\n tmpPrefix = File(System.getProperty(\"java.io.tmpdir\"), \"cc-test-prefix-${System.nanoTime()}\")\n tmpPrefix.mkdirs()\n tmpHome = File(tmpPrefix, \"home\").apply { mkdirs() }\n bootstrapper = mockk(relaxed = true)\n every { bootstrapper.prefixDir } returns tmpPrefix\n every { bootstrapper.homeDir } returns tmpHome\n\n ptyEnvironment = mockk(relaxed = true)\n every { ptyEnvironment.envp() } returns arrayOf(\n \"PATH=/usr/bin:/bin\",\n \"HOME=${tmpHome.absolutePath}\",\n \"OPENAI_API_KEY=sk-leak-this-secret\",\n \"ANTHROPIC_API_KEY=anth-leak\",\n \"DEEPSEEK_API_KEY=dsk-leak\",\n \"CC_UI_TOKEN=ui-leak\",\n \"DASHSCOPE_API_KEY=qwen-leak\",\n \"VOLCENGINE_API_KEY=doubao-leak\",\n \"LANG=en_US.UTF-8\",\n \"MY_HARMLESS=value-with=equals=in=it\",\n \"EMPTY=\",\n \"=should-be-skipped-no-key\",\n )\n\n service = CcExecService(bootstrapper, ptyEnvironment)\n }\n\n // ---------- argv ----------\n\n @Test fun `buildArgv composes node ccJs command subargs`() {\n val argv = service.buildArgv(\"/n/node\", \"/cc.js\", \"note\", listOf(\"list\", \"--limit\", \"10\"))\n assertEquals(listOf(\"/n/node\", \"/cc.js\", \"note\", \"list\", \"--limit\", \"10\"), argv)\n }\n\n @Test fun `buildArgv with empty subargs`() {\n val argv = service.buildArgv(\"/n/node\", \"/cc.js\", \"status\", emptyList())\n assertEquals(listOf(\"/n/node\", \"/cc.js\", \"status\"), argv)\n }\n\n // ---------- env filter ----------\n\n @Test fun `buildFilteredEnv strips secret-prefix vars`() {\n val env = service.buildFilteredEnv()\n for (prefix in CcExecService.FORBIDDEN_ENV_PREFIXES) {\n assertTrue(\n env.keys.none { it.startsWith(prefix) },\n \"should have stripped $prefix; got keys: ${env.keys}\"\n )\n }\n }\n\n @Test fun `buildFilteredEnv preserves benign vars`() {\n val env = service.buildFilteredEnv()\n assertEquals(\"/usr/bin:/bin\", env[\"PATH\"])\n assertEquals(\"en_US.UTF-8\", env[\"LANG\"])\n assertEquals(tmpHome.absolutePath, env[\"HOME\"])\n }\n\n @Test fun `buildFilteredEnv preserves vars with = in value`() {\n val env = service.buildFilteredEnv()\n // indexOf('=') uses FIRST '=' as separator → value can contain =\n assertEquals(\"value-with=equals=in=it\", env[\"MY_HARMLESS\"])\n }\n\n @Test fun `buildFilteredEnv keeps empty values (cc CLI distinguishes set-empty vs unset)`() {\n val env = service.buildFilteredEnv()\n // EMPTY= → \"\" preserved (idx=5, k=\"EMPTY\", v=\"\")\n assertTrue(env.containsKey(\"EMPTY\"))\n assertEquals(\"\", env[\"EMPTY\"])\n }\n\n @Test fun `buildFilteredEnv skips malformed entries with no key`() {\n val env = service.buildFilteredEnv()\n // \"=should-be-skipped-no-key\" → idx=0 → idx <= 0 skip\n assertFalse(env.containsValue(\"should-be-skipped-no-key\"))\n }\n\n // ---------- truncation ----------\n\n @Test fun `decodeAndTruncate under limit returns full UTF8`() {\n val bytes = \"hello 你好\".toByteArray(Charsets.UTF_8)\n val out = service.decodeAndTruncate(bytes, 100)\n assertEquals(\"hello 你好\", out)\n }\n\n @Test fun `decodeAndTruncate over limit adds marker`() {\n val bytes = ByteArray(2000) { it.toByte() }\n val out = service.decodeAndTruncate(bytes, 100)\n assertTrue(out.contains(\"TRUNCATED\"))\n assertTrue(out.contains(\"total=2000\"))\n }\n\n @Test fun `decodeAndTruncate empty input`() {\n val out = service.decodeAndTruncate(ByteArray(0), 100)\n assertEquals(\"\", out)\n }\n\n // ---------- run() — missing binaries return clean Error ----------\n\n @Test fun `run returns Error when node binary missing`() = runTest {\n // tmpPrefix/bin/node does NOT exist\n val r = service.run(\"status\", emptyList())\n assertTrue(r is CcResult.Error)\n val err = r as CcResult.Error\n assertTrue(err.reason.contains(\"Node binary missing\"))\n assertNotNull(err.expectedPath)\n assertTrue(err.expectedPath!!.endsWith(\"/bin/node\"))\n }\n\n @Test fun `run returns Error when ccJs missing (node exists)`() = runTest {\n // create stub node binary, not ccJs\n File(tmpPrefix, \"bin\").mkdirs()\n File(tmpPrefix, \"bin/node\").apply {\n createNewFile()\n setExecutable(true)\n }\n val r = service.run(\"status\", emptyList())\n assertTrue(r is CcResult.Error)\n val err = r as CcResult.Error\n assertTrue(err.reason.contains(\"cc CLI snapshot missing\"))\n }\n\n // ---------- B28 fix — async stream drain prevents pipe-buffer deadlock ----------\n //\n // We can't run real `node + cc CLI` in jvmTest (no Termux binary). But we\n // CAN verify the drain path with a host-shell that emits >64KB stdout —\n // that's what would have deadlocked the pre-fix code. On Windows host we\n // skip (uses cmd.exe quoting differences); on linux/mac CI it runs.\n\n @Test fun `B28 — executeArgv drains large stdout without deadlock`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"yes hello | head -c 200000\")\n // Only run if /bin/sh exists (linux/mac); Windows test JVM lacks it.\n if (!File(\"/bin/sh\").exists()) return@runTest\n\n val r = service.executeArgv(\n argv = sh,\n env = mapOf(\"PATH\" to \"/usr/bin:/bin\"),\n cwd = tmpHome,\n timeoutMs = 10_000L,\n )\n assertTrue(r is CcResult.Ok, \"expected Ok; got $r\")\n val ok = r as CcResult.Ok\n assertEquals(0, ok.exitCode)\n // 200KB stdout consumed; result truncated to STDOUT_TRUNCATE_BYTES (4KB)\n assertTrue(ok.stdout.contains(\"TRUNCATED\"))\n assertTrue(ok.stdout.contains(\"total=200000\"))\n }\n\n @Test fun `executeArgv reports non-zero exit`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"exit 42\")\n if (!File(\"/bin/sh\").exists()) return@runTest\n val r = service.executeArgv(sh, mapOf(\"PATH\" to \"/usr/bin:/bin\"), tmpHome, 5_000L)\n assertTrue(r is CcResult.Ok)\n assertEquals(42, (r as CcResult.Ok).exitCode)\n }\n\n @Test fun `executeArgv reports stderr separately`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"echo good; echo BAD >&2; exit 0\")\n if (!File(\"/bin/sh\").exists()) return@runTest\n val r = service.executeArgv(sh, mapOf(\"PATH\" to \"/usr/bin:/bin\"), tmpHome, 5_000L)\n assertTrue(r is CcResult.Ok)\n val ok = r as CcResult.Ok\n assertTrue(ok.stdout.contains(\"good\"))\n assertTrue(ok.stderr.contains(\"BAD\"))\n }\n\n @Test fun `executeArgv timeout kills runaway`() = runTest {\n val sh = listOf(\"/bin/sh\", \"-c\", \"sleep 30\")\n if (!File(\"/bin/sh\").exists()) return@runTest\n val started = System.currentTimeMillis()\n val r = service.executeArgv(sh, mapOf(\"PATH\" to \"/usr/bin:/bin\"), tmpHome, 500L)\n val elapsed = System.currentTimeMillis() - started\n assertTrue(r is CcResult.Error)\n assertTrue((r as CcResult.Error).reason.contains(\"timeout\"))\n // confirm we didn't actually wait the full sleep (process killed)\n assertTrue(elapsed < 5_000L, \"timeout took too long: ${elapsed}ms\")\n }\n\n @Test fun `executeArgv ProcessBuilder failure returns Error`() = runTest {\n val r = service.executeArgv(\n argv = listOf(\"/this/path/definitely/does/not/exist\"),\n env = mapOf(\"PATH\" to \"/usr/bin\"),\n cwd = tmpHome,\n timeoutMs = 1_000L,\n )\n assertTrue(r is CcResult.Error)\n assertTrue((r as CcResult.Error).reason.contains(\"ProcessBuilder\"))\n }\n}\n"}}, "desktop-app-vue/src/main/llm/openai-client.js": {"9e2a3238426499a3de1d228034e66dab91cbfa2c": {"t": 1785746011.821464, "n": 12605, "h": "1d13f77b14737956", "c": "/**\n * OpenAI 兼容 API 客户端\n *\n * 支持: OpenAI, DeepSeek, 以及其他兼容OpenAI API的服务\n */\n\nconst { logger } = require(\"../utils/logger.js\");\nconst axios = require(\"axios\");\nconst EventEmitter = require(\"events\");\n\n/**\n * OpenAI兼容客户端类\n */\nclass OpenAIClient extends EventEmitter {\n constructor(config = {}) {\n super();\n\n // API配置\n this.apiKey = config.apiKey;\n this.baseURL = config.baseURL || \"https://api.openai.com/v1\";\n this.model = config.model || \"gpt-3.5-turbo\";\n this.embeddingModel = config.embeddingModel || \"text-embedding-ada-002\";\n this.timeout = config.timeout || 300000; // 5 minutes default\n this.maxRetries = config.maxRetries || 2; // Retry up to 2 times on timeout\n this.organization = config.organization;\n\n // 创建axios实例\n this.client = axios.create({\n baseURL: this.baseURL,\n timeout: this.timeout,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),\n ...(this.organization && { \"OpenAI-Organization\": this.organization }),\n },\n });\n }\n\n /**\n * 检查服务状态\n */\n async checkStatus() {\n try {\n // 火山引擎特殊处理:使用简单的聊天测试代替模型列表\n if (this.baseURL && this.baseURL.includes(\"volces.com\")) {\n logger.info(\"[OpenAIClient] 检测到火山引擎,使用聊天测试代替模型列表\");\n\n const testResponse = await this.client.post(\n \"/chat/completions\",\n {\n model: this.model,\n messages: [{ role: \"user\", content: \"hi\" }],\n max_tokens: 5,\n },\n {\n timeout: 10000, // 10秒快速超时\n },\n );\n\n return {\n available: true,\n models: [\n {\n name: this.model,\n created: Date.now(),\n owned_by: \"volcengine\",\n },\n ],\n testResponse: testResponse.data,\n };\n }\n\n // 标准OpenAI API:尝试列出模型\n const response = await this.client.get(\"/models\");\n\n const models = response.data.data || [];\n\n return {\n available: true,\n models: models.map((m) => ({\n name: m.id,\n created: m.created,\n owned_by: m.owned_by,\n })),\n };\n } catch (error) {\n return {\n available: false,\n error: error.response?.data?.error?.message || error.message,\n models: [],\n };\n }\n }\n\n /**\n * 聊天补全(非流式)\n * @param {Array} messages - 消息数组\n * @param {Object} options - 选项\n */\n async chat(messages, options = {}) {\n const maxRetries = options.maxRetries ?? this.maxRetries;\n let lastError = null;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n if (attempt > 0) {\n logger.info(`[OpenAIClient] 重试第 ${attempt} 次...`);\n }\n\n // 构建请求体\n const requestBody = {\n model: options.model || this.model,\n messages,\n temperature: options.temperature || 0.7,\n top_p: options.top_p || 1,\n max_tokens: options.max_tokens,\n presence_penalty: options.presence_penalty || 0,\n frequency_penalty: options.frequency_penalty || 0,\n stream: false,\n };\n\n // 🔥 修复:只有在 tools 有效且非空时才添加(避免阿里云等API报错)\n if (\n options.tools &&\n Array.isArray(options.tools) &&\n options.tools.length > 0\n ) {\n // 验证每个tool都有必要的字段\n const validTools = options.tools.filter((tool) => {\n if (tool.type === \"function\") {\n return tool.function && tool.function.name;\n }\n return true; // 其他类型的工具暂时允许\n });\n\n if (validTools.length > 0) {\n requestBody.tools = validTools;\n }\n }\n\n const response = await this.client.post(\n \"/chat/completions\",\n requestBody,\n {\n ...(options.signal && { signal: options.signal }),\n },\n );\n\n const choice = response.data.choices[0];\n\n return {\n message: choice.message,\n finish_reason: choice.finish_reason,\n model: response.data.model,\n usage: response.data.usage,\n tokens: response.data.usage?.total_tokens || 0,\n };\n } catch (error) {\n lastError = error;\n const isTimeout =\n error.code === \"ECONNABORTED\" || error.message?.includes(\"timeout\");\n const isNetworkError =\n error.code === \"ECONNRESET\" || error.code === \"ENOTFOUND\";\n\n // Only retry on timeout or network errors\n if ((isTimeout || isNetworkError) && attempt < maxRetries) {\n logger.warn(\n `[OpenAIClient] 请求超时或网络错误,将重试: ${error.message}`,\n );\n // Wait before retry (exponential backoff: 2s, 4s)\n await new Promise((resolve) =>\n setTimeout(resolve, 2000 * Math.pow(2, attempt)),\n );\n continue;\n }\n\n logger.error(\"[OpenAIClient] 聊天失败:\", error.response?.data || error);\n throw new Error(this._formatAPIError(error));\n }\n }\n\n // Should not reach here, but just in case\n throw lastError;\n }\n\n /**\n * 聊天补全(流式)\n * @param {Array} messages - 消息数组\n * @param {Function} onChunk - 回调函数\n * @param {Object} options - 选项\n */\n async chatStream(messages, onChunk, options = {}) {\n try {\n // 构建请求体\n const requestBody = {\n model: options.model || this.model,\n messages,\n temperature: options.temperature || 0.7,\n top_p: options.top_p || 1,\n max_tokens: options.max_tokens,\n presence_penalty: options.presence_penalty || 0,\n frequency_penalty: options.frequency_penalty || 0,\n stream: true,\n };\n\n // 🔥 修复:只有在 tools 有效且非空时才添加(避免阿里云等API报错)\n if (\n options.tools &&\n Array.isArray(options.tools) &&\n options.tools.length > 0\n ) {\n // 验证每个tool都有必要的字段\n const validTools = options.tools.filter((tool) => {\n if (tool.type === \"function\") {\n return tool.function && tool.function.name;\n }\n return true; // 其他类型的工具暂时允许\n });\n\n if (validTools.length > 0) {\n requestBody.tools = validTools;\n }\n }\n\n const response = await this.client.post(\n \"/chat/completions\",\n requestBody,\n {\n responseType: \"stream\",\n },\n );\n\n const fullMessage = {\n role: \"assistant\",\n content: \"\",\n };\n\n return new Promise((resolve, reject) => {\n let buffer = \"\";\n response.data.on(\"data\", (chunk) => {\n // Buffer across 'data' events: a single SSE \"data:\" frame can be split\n // across TCP chunks; parsing each chunk independently dropped the\n // halves (silently losing tokens). anthropic/gemini clients buffer;\n // this one didn't. Keep the trailing incomplete line in the buffer.\n buffer += chunk.toString();\n const parts = buffer.split(\"\\n\");\n buffer = parts.pop() || \"\";\n const lines = parts.filter((line) =>\n line.trim().startsWith(\"data: \"),\n );\n\n for (const line of lines) {\n const data = line.trim().replace(/^data: /, \"\");\n\n if (data === \"[DONE]\") {\n resolve({\n message: fullMessage,\n model: options.model || this.model,\n finish_reason: \"stop\",\n });\n return;\n }\n\n try {\n const parsed = JSON.parse(data);\n const delta = parsed.choices[0]?.delta;\n\n if (delta?.content) {\n fullMessage.content += delta.content;\n // 传递对象格式的chunk,保持统一接口\n onChunk({\n content: delta.content,\n delta: delta,\n fullContent: fullMessage.content,\n });\n }\n\n if (parsed.choices[0]?.finish_reason) {\n resolve({\n message: fullMessage,\n model: parsed.model,\n finish_reason: parsed.choices[0].finish_reason,\n });\n }\n } catch (e) {\n // 忽略解析错误\n }\n }\n });\n\n response.data.on(\"error\", (error) => {\n reject(error);\n });\n\n response.data.on(\"end\", () => {\n // 如果没有收到[DONE],也要resolve\n resolve({\n message: fullMessage,\n model: options.model || this.model,\n finish_reason: \"stop\",\n });\n });\n });\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 流式聊天失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 文本补全(非流式)- 兼容老版API\n * @param {string} prompt - 提示词\n * @param {Object} options - 选项\n */\n async complete(prompt, options = {}) {\n try {\n const response = await this.client.post(\"/completions\", {\n model: options.model || \"gpt-3.5-turbo-instruct\",\n prompt,\n temperature: options.temperature || 0.7,\n max_tokens: options.max_tokens || 1000,\n top_p: options.top_p || 1,\n stream: false,\n });\n\n const choice = response.data.choices[0];\n\n return {\n text: choice.text,\n finish_reason: choice.finish_reason,\n model: response.data.model,\n usage: response.data.usage,\n tokens: response.data.usage?.total_tokens || 0,\n };\n } catch (error) {\n logger.error(\"[OpenAIClient] 补全失败:\", error.response?.data || error);\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 生成嵌入向量\n * @param {string|Array} input - 文本或文本数组\n * @param {string} model - 模型(可选,默认使用配置的embeddingModel)\n */\n async embeddings(input, model = null) {\n try {\n const embeddingModel = model || this.embeddingModel;\n\n const response = await this.client.post(\"/embeddings\", {\n model: embeddingModel,\n input,\n });\n\n if (Array.isArray(input)) {\n return response.data.data.map((d) => d.embedding);\n } else {\n return response.data.data[0].embedding;\n }\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 生成嵌入失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 列出可用模型\n */\n async listModels() {\n try {\n const response = await this.client.get(\"/models\");\n return response.data.data;\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 列出模型失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 获取模型信息\n * @param {string} modelId - 模型ID\n */\n async getModel(modelId) {\n try {\n const response = await this.client.get(`/models/${modelId}`);\n return response.data;\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 获取模型信息失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 格式化 API 错误为用户友好的消息\n * @param {Error} error - axios 错误对象\n * @returns {string} 用户友好的错误消息\n */\n _formatAPIError(error) {\n const status = error.response?.status;\n const serverMessage = error.response?.data?.error?.message;\n const baseURL = this.baseURL || \"\";\n\n switch (status) {\n case 401:\n return `API 密钥无效或已过期,请在设置中检查 API Key 配置(${baseURL})`;\n case 403:\n return `API 访问被拒绝,请检查 API Key 权限或账户状态(${baseURL})`;\n case 429:\n return `API 请求频率超限或额度用尽,请稍后重试或检查账户余额(${baseURL})`;\n case 500:\n case 502:\n case 503:\n return `API 服务暂时不可用(HTTP ${status}),请稍后重试(${baseURL})`;\n case 404:\n return `API 端点不存在或模型不可用,请检查 API 地址和模型配置(${baseURL})`;\n default:\n if (\n error.code === \"ECONNABORTED\" ||\n error.message?.includes(\"timeout\")\n ) {\n return `API 请求超时,请检查网络连接或稍后重试(${baseURL})`;\n }\n if (error.code === \"ECONNREFUSED\") {\n return `无法连接到 API 服务,请检查服务地址是否正确(${baseURL})`;\n }\n if (error.code === \"ENOTFOUND\") {\n return `无法解析 API 服务地址,请检查网络连接和 API 地址配置(${baseURL})`;\n }\n return serverMessage || error.message;\n }\n }\n}\n\n/**\n * DeepSeek 客户端 (使用OpenAI兼容API)\n */\nclass DeepSeekClient extends OpenAIClient {\n constructor(config = {}) {\n super({\n ...config,\n baseURL: config.baseURL || \"https://api.deepseek.com/v1\",\n model: config.model || \"deepseek-chat\",\n });\n }\n}\n\nmodule.exports = {\n OpenAIClient,\n DeepSeekClient,\n};\n"}, "518136feec6cee419f33427a84817b3d5cd94ab8": {"t": 1785754068.6571877, "n": 12605, "h": "1d13f77b14737956", "c": "/**\n * OpenAI 兼容 API 客户端\n *\n * 支持: OpenAI, DeepSeek, 以及其他兼容OpenAI API的服务\n */\n\nconst { logger } = require(\"../utils/logger.js\");\nconst axios = require(\"axios\");\nconst EventEmitter = require(\"events\");\n\n/**\n * OpenAI兼容客户端类\n */\nclass OpenAIClient extends EventEmitter {\n constructor(config = {}) {\n super();\n\n // API配置\n this.apiKey = config.apiKey;\n this.baseURL = config.baseURL || \"https://api.openai.com/v1\";\n this.model = config.model || \"gpt-3.5-turbo\";\n this.embeddingModel = config.embeddingModel || \"text-embedding-ada-002\";\n this.timeout = config.timeout || 300000; // 5 minutes default\n this.maxRetries = config.maxRetries || 2; // Retry up to 2 times on timeout\n this.organization = config.organization;\n\n // 创建axios实例\n this.client = axios.create({\n baseURL: this.baseURL,\n timeout: this.timeout,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),\n ...(this.organization && { \"OpenAI-Organization\": this.organization }),\n },\n });\n }\n\n /**\n * 检查服务状态\n */\n async checkStatus() {\n try {\n // 火山引擎特殊处理:使用简单的聊天测试代替模型列表\n if (this.baseURL && this.baseURL.includes(\"volces.com\")) {\n logger.info(\"[OpenAIClient] 检测到火山引擎,使用聊天测试代替模型列表\");\n\n const testResponse = await this.client.post(\n \"/chat/completions\",\n {\n model: this.model,\n messages: [{ role: \"user\", content: \"hi\" }],\n max_tokens: 5,\n },\n {\n timeout: 10000, // 10秒快速超时\n },\n );\n\n return {\n available: true,\n models: [\n {\n name: this.model,\n created: Date.now(),\n owned_by: \"volcengine\",\n },\n ],\n testResponse: testResponse.data,\n };\n }\n\n // 标准OpenAI API:尝试列出模型\n const response = await this.client.get(\"/models\");\n\n const models = response.data.data || [];\n\n return {\n available: true,\n models: models.map((m) => ({\n name: m.id,\n created: m.created,\n owned_by: m.owned_by,\n })),\n };\n } catch (error) {\n return {\n available: false,\n error: error.response?.data?.error?.message || error.message,\n models: [],\n };\n }\n }\n\n /**\n * 聊天补全(非流式)\n * @param {Array} messages - 消息数组\n * @param {Object} options - 选项\n */\n async chat(messages, options = {}) {\n const maxRetries = options.maxRetries ?? this.maxRetries;\n let lastError = null;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n if (attempt > 0) {\n logger.info(`[OpenAIClient] 重试第 ${attempt} 次...`);\n }\n\n // 构建请求体\n const requestBody = {\n model: options.model || this.model,\n messages,\n temperature: options.temperature || 0.7,\n top_p: options.top_p || 1,\n max_tokens: options.max_tokens,\n presence_penalty: options.presence_penalty || 0,\n frequency_penalty: options.frequency_penalty || 0,\n stream: false,\n };\n\n // 🔥 修复:只有在 tools 有效且非空时才添加(避免阿里云等API报错)\n if (\n options.tools &&\n Array.isArray(options.tools) &&\n options.tools.length > 0\n ) {\n // 验证每个tool都有必要的字段\n const validTools = options.tools.filter((tool) => {\n if (tool.type === \"function\") {\n return tool.function && tool.function.name;\n }\n return true; // 其他类型的工具暂时允许\n });\n\n if (validTools.length > 0) {\n requestBody.tools = validTools;\n }\n }\n\n const response = await this.client.post(\n \"/chat/completions\",\n requestBody,\n {\n ...(options.signal && { signal: options.signal }),\n },\n );\n\n const choice = response.data.choices[0];\n\n return {\n message: choice.message,\n finish_reason: choice.finish_reason,\n model: response.data.model,\n usage: response.data.usage,\n tokens: response.data.usage?.total_tokens || 0,\n };\n } catch (error) {\n lastError = error;\n const isTimeout =\n error.code === \"ECONNABORTED\" || error.message?.includes(\"timeout\");\n const isNetworkError =\n error.code === \"ECONNRESET\" || error.code === \"ENOTFOUND\";\n\n // Only retry on timeout or network errors\n if ((isTimeout || isNetworkError) && attempt < maxRetries) {\n logger.warn(\n `[OpenAIClient] 请求超时或网络错误,将重试: ${error.message}`,\n );\n // Wait before retry (exponential backoff: 2s, 4s)\n await new Promise((resolve) =>\n setTimeout(resolve, 2000 * Math.pow(2, attempt)),\n );\n continue;\n }\n\n logger.error(\"[OpenAIClient] 聊天失败:\", error.response?.data || error);\n throw new Error(this._formatAPIError(error));\n }\n }\n\n // Should not reach here, but just in case\n throw lastError;\n }\n\n /**\n * 聊天补全(流式)\n * @param {Array} messages - 消息数组\n * @param {Function} onChunk - 回调函数\n * @param {Object} options - 选项\n */\n async chatStream(messages, onChunk, options = {}) {\n try {\n // 构建请求体\n const requestBody = {\n model: options.model || this.model,\n messages,\n temperature: options.temperature || 0.7,\n top_p: options.top_p || 1,\n max_tokens: options.max_tokens,\n presence_penalty: options.presence_penalty || 0,\n frequency_penalty: options.frequency_penalty || 0,\n stream: true,\n };\n\n // 🔥 修复:只有在 tools 有效且非空时才添加(避免阿里云等API报错)\n if (\n options.tools &&\n Array.isArray(options.tools) &&\n options.tools.length > 0\n ) {\n // 验证每个tool都有必要的字段\n const validTools = options.tools.filter((tool) => {\n if (tool.type === \"function\") {\n return tool.function && tool.function.name;\n }\n return true; // 其他类型的工具暂时允许\n });\n\n if (validTools.length > 0) {\n requestBody.tools = validTools;\n }\n }\n\n const response = await this.client.post(\n \"/chat/completions\",\n requestBody,\n {\n responseType: \"stream\",\n },\n );\n\n const fullMessage = {\n role: \"assistant\",\n content: \"\",\n };\n\n return new Promise((resolve, reject) => {\n let buffer = \"\";\n response.data.on(\"data\", (chunk) => {\n // Buffer across 'data' events: a single SSE \"data:\" frame can be split\n // across TCP chunks; parsing each chunk independently dropped the\n // halves (silently losing tokens). anthropic/gemini clients buffer;\n // this one didn't. Keep the trailing incomplete line in the buffer.\n buffer += chunk.toString();\n const parts = buffer.split(\"\\n\");\n buffer = parts.pop() || \"\";\n const lines = parts.filter((line) =>\n line.trim().startsWith(\"data: \"),\n );\n\n for (const line of lines) {\n const data = line.trim().replace(/^data: /, \"\");\n\n if (data === \"[DONE]\") {\n resolve({\n message: fullMessage,\n model: options.model || this.model,\n finish_reason: \"stop\",\n });\n return;\n }\n\n try {\n const parsed = JSON.parse(data);\n const delta = parsed.choices[0]?.delta;\n\n if (delta?.content) {\n fullMessage.content += delta.content;\n // 传递对象格式的chunk,保持统一接口\n onChunk({\n content: delta.content,\n delta: delta,\n fullContent: fullMessage.content,\n });\n }\n\n if (parsed.choices[0]?.finish_reason) {\n resolve({\n message: fullMessage,\n model: parsed.model,\n finish_reason: parsed.choices[0].finish_reason,\n });\n }\n } catch (e) {\n // 忽略解析错误\n }\n }\n });\n\n response.data.on(\"error\", (error) => {\n reject(error);\n });\n\n response.data.on(\"end\", () => {\n // 如果没有收到[DONE],也要resolve\n resolve({\n message: fullMessage,\n model: options.model || this.model,\n finish_reason: \"stop\",\n });\n });\n });\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 流式聊天失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 文本补全(非流式)- 兼容老版API\n * @param {string} prompt - 提示词\n * @param {Object} options - 选项\n */\n async complete(prompt, options = {}) {\n try {\n const response = await this.client.post(\"/completions\", {\n model: options.model || \"gpt-3.5-turbo-instruct\",\n prompt,\n temperature: options.temperature || 0.7,\n max_tokens: options.max_tokens || 1000,\n top_p: options.top_p || 1,\n stream: false,\n });\n\n const choice = response.data.choices[0];\n\n return {\n text: choice.text,\n finish_reason: choice.finish_reason,\n model: response.data.model,\n usage: response.data.usage,\n tokens: response.data.usage?.total_tokens || 0,\n };\n } catch (error) {\n logger.error(\"[OpenAIClient] 补全失败:\", error.response?.data || error);\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 生成嵌入向量\n * @param {string|Array} input - 文本或文本数组\n * @param {string} model - 模型(可选,默认使用配置的embeddingModel)\n */\n async embeddings(input, model = null) {\n try {\n const embeddingModel = model || this.embeddingModel;\n\n const response = await this.client.post(\"/embeddings\", {\n model: embeddingModel,\n input,\n });\n\n if (Array.isArray(input)) {\n return response.data.data.map((d) => d.embedding);\n } else {\n return response.data.data[0].embedding;\n }\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 生成嵌入失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 列出可用模型\n */\n async listModels() {\n try {\n const response = await this.client.get(\"/models\");\n return response.data.data;\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 列出模型失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 获取模型信息\n * @param {string} modelId - 模型ID\n */\n async getModel(modelId) {\n try {\n const response = await this.client.get(`/models/${modelId}`);\n return response.data;\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 获取模型信息失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 格式化 API 错误为用户友好的消息\n * @param {Error} error - axios 错误对象\n * @returns {string} 用户友好的错误消息\n */\n _formatAPIError(error) {\n const status = error.response?.status;\n const serverMessage = error.response?.data?.error?.message;\n const baseURL = this.baseURL || \"\";\n\n switch (status) {\n case 401:\n return `API 密钥无效或已过期,请在设置中检查 API Key 配置(${baseURL})`;\n case 403:\n return `API 访问被拒绝,请检查 API Key 权限或账户状态(${baseURL})`;\n case 429:\n return `API 请求频率超限或额度用尽,请稍后重试或检查账户余额(${baseURL})`;\n case 500:\n case 502:\n case 503:\n return `API 服务暂时不可用(HTTP ${status}),请稍后重试(${baseURL})`;\n case 404:\n return `API 端点不存在或模型不可用,请检查 API 地址和模型配置(${baseURL})`;\n default:\n if (\n error.code === \"ECONNABORTED\" ||\n error.message?.includes(\"timeout\")\n ) {\n return `API 请求超时,请检查网络连接或稍后重试(${baseURL})`;\n }\n if (error.code === \"ECONNREFUSED\") {\n return `无法连接到 API 服务,请检查服务地址是否正确(${baseURL})`;\n }\n if (error.code === \"ENOTFOUND\") {\n return `无法解析 API 服务地址,请检查网络连接和 API 地址配置(${baseURL})`;\n }\n return serverMessage || error.message;\n }\n }\n}\n\n/**\n * DeepSeek 客户端 (使用OpenAI兼容API)\n */\nclass DeepSeekClient extends OpenAIClient {\n constructor(config = {}) {\n super({\n ...config,\n baseURL: config.baseURL || \"https://api.deepseek.com/v1\",\n model: config.model || \"deepseek-chat\",\n });\n }\n}\n\nmodule.exports = {\n OpenAIClient,\n DeepSeekClient,\n};\n"}, "eca79c5eb7f950139df1b21052ec1b828873d9cd": {"t": 1785754070.0728066, "n": 12174, "h": "35a3e0ec3011953a", "c": "/**\n * OpenAI 兼容 API 客户端\n *\n * 支持: OpenAI, DeepSeek, 以及其他兼容OpenAI API的服务\n */\n\nconst { logger } = require(\"../utils/logger.js\");\nconst axios = require(\"axios\");\nconst EventEmitter = require(\"events\");\n\n/**\n * OpenAI兼容客户端类\n */\nclass OpenAIClient extends EventEmitter {\n constructor(config = {}) {\n super();\n\n // API配置\n this.apiKey = config.apiKey;\n this.baseURL = config.baseURL || \"https://api.openai.com/v1\";\n this.model = config.model || \"gpt-3.5-turbo\";\n this.embeddingModel = config.embeddingModel || \"text-embedding-ada-002\";\n this.timeout = config.timeout || 300000; // 5 minutes default\n this.maxRetries = config.maxRetries || 2; // Retry up to 2 times on timeout\n this.organization = config.organization;\n\n // 创建axios实例\n this.client = axios.create({\n baseURL: this.baseURL,\n timeout: this.timeout,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),\n ...(this.organization && { \"OpenAI-Organization\": this.organization }),\n },\n });\n }\n\n /**\n * 检查服务状态\n */\n async checkStatus() {\n try {\n // 火山引擎特殊处理:使用简单的聊天测试代替模型列表\n if (this.baseURL && this.baseURL.includes(\"volces.com\")) {\n logger.info(\"[OpenAIClient] 检测到火山引擎,使用聊天测试代替模型列表\");\n\n const testResponse = await this.client.post(\n \"/chat/completions\",\n {\n model: this.model,\n messages: [{ role: \"user\", content: \"hi\" }],\n max_tokens: 5,\n },\n {\n timeout: 10000, // 10秒快速超时\n },\n );\n\n return {\n available: true,\n models: [\n {\n name: this.model,\n created: Date.now(),\n owned_by: \"volcengine\",\n },\n ],\n testResponse: testResponse.data,\n };\n }\n\n // 标准OpenAI API:尝试列出模型\n const response = await this.client.get(\"/models\");\n\n const models = response.data.data || [];\n\n return {\n available: true,\n models: models.map((m) => ({\n name: m.id,\n created: m.created,\n owned_by: m.owned_by,\n })),\n };\n } catch (error) {\n return {\n available: false,\n error: error.response?.data?.error?.message || error.message,\n models: [],\n };\n }\n }\n\n /**\n * 聊天补全(非流式)\n * @param {Array} messages - 消息数组\n * @param {Object} options - 选项\n */\n async chat(messages, options = {}) {\n const maxRetries = options.maxRetries ?? this.maxRetries;\n let lastError = null;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n if (attempt > 0) {\n logger.info(`[OpenAIClient] 重试第 ${attempt} 次...`);\n }\n\n // 构建请求体\n const requestBody = {\n model: options.model || this.model,\n messages,\n temperature: options.temperature || 0.7,\n top_p: options.top_p || 1,\n max_tokens: options.max_tokens,\n presence_penalty: options.presence_penalty || 0,\n frequency_penalty: options.frequency_penalty || 0,\n stream: false,\n };\n\n // 🔥 修复:只有在 tools 有效且非空时才添加(避免阿里云等API报错)\n if (\n options.tools &&\n Array.isArray(options.tools) &&\n options.tools.length > 0\n ) {\n // 验证每个tool都有必要的字段\n const validTools = options.tools.filter((tool) => {\n if (tool.type === \"function\") {\n return tool.function && tool.function.name;\n }\n return true; // 其他类型的工具暂时允许\n });\n\n if (validTools.length > 0) {\n requestBody.tools = validTools;\n }\n }\n\n const response = await this.client.post(\n \"/chat/completions\",\n requestBody,\n {\n ...(options.signal && { signal: options.signal }),\n },\n );\n\n const choice = response.data.choices[0];\n\n return {\n message: choice.message,\n finish_reason: choice.finish_reason,\n model: response.data.model,\n usage: response.data.usage,\n tokens: response.data.usage?.total_tokens || 0,\n };\n } catch (error) {\n lastError = error;\n const isTimeout =\n error.code === \"ECONNABORTED\" || error.message?.includes(\"timeout\");\n const isNetworkError =\n error.code === \"ECONNRESET\" || error.code === \"ENOTFOUND\";\n\n // Only retry on timeout or network errors\n if ((isTimeout || isNetworkError) && attempt < maxRetries) {\n logger.warn(\n `[OpenAIClient] 请求超时或网络错误,将重试: ${error.message}`,\n );\n // Wait before retry (exponential backoff: 2s, 4s)\n await new Promise((resolve) =>\n setTimeout(resolve, 2000 * Math.pow(2, attempt)),\n );\n continue;\n }\n\n logger.error(\"[OpenAIClient] 聊天失败:\", error.response?.data || error);\n throw new Error(this._formatAPIError(error));\n }\n }\n\n // Should not reach here, but just in case\n throw lastError;\n }\n\n /**\n * 聊天补全(流式)\n * @param {Array} messages - 消息数组\n * @param {Function} onChunk - 回调函数\n * @param {Object} options - 选项\n */\n async chatStream(messages, onChunk, options = {}) {\n try {\n // 构建请求体\n const requestBody = {\n model: options.model || this.model,\n messages,\n temperature: options.temperature || 0.7,\n top_p: options.top_p || 1,\n max_tokens: options.max_tokens,\n presence_penalty: options.presence_penalty || 0,\n frequency_penalty: options.frequency_penalty || 0,\n stream: true,\n };\n\n // 🔥 修复:只有在 tools 有效且非空时才添加(避免阿里云等API报错)\n if (\n options.tools &&\n Array.isArray(options.tools) &&\n options.tools.length > 0\n ) {\n // 验证每个tool都有必要的字段\n const validTools = options.tools.filter((tool) => {\n if (tool.type === \"function\") {\n return tool.function && tool.function.name;\n }\n return true; // 其他类型的工具暂时允许\n });\n\n if (validTools.length > 0) {\n requestBody.tools = validTools;\n }\n }\n\n const response = await this.client.post(\n \"/chat/completions\",\n requestBody,\n {\n responseType: \"stream\",\n },\n );\n\n const fullMessage = {\n role: \"assistant\",\n content: \"\",\n };\n\n return new Promise((resolve, reject) => {\n response.data.on(\"data\", (chunk) => {\n const lines = chunk\n .toString()\n .split(\"\\n\")\n .filter((line) => line.trim().startsWith(\"data: \"));\n\n for (const line of lines) {\n const data = line.replace(/^data: /, \"\");\n\n if (data === \"[DONE]\") {\n resolve({\n message: fullMessage,\n model: options.model || this.model,\n finish_reason: \"stop\",\n });\n return;\n }\n\n try {\n const parsed = JSON.parse(data);\n const delta = parsed.choices[0]?.delta;\n\n if (delta?.content) {\n fullMessage.content += delta.content;\n // 传递对象格式的chunk,保持统一接口\n onChunk({\n content: delta.content,\n delta: delta,\n fullContent: fullMessage.content,\n });\n }\n\n if (parsed.choices[0]?.finish_reason) {\n resolve({\n message: fullMessage,\n model: parsed.model,\n finish_reason: parsed.choices[0].finish_reason,\n });\n }\n } catch (e) {\n // 忽略解析错误\n }\n }\n });\n\n response.data.on(\"error\", (error) => {\n reject(error);\n });\n\n response.data.on(\"end\", () => {\n // 如果没有收到[DONE],也要resolve\n resolve({\n message: fullMessage,\n model: options.model || this.model,\n finish_reason: \"stop\",\n });\n });\n });\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 流式聊天失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 文本补全(非流式)- 兼容老版API\n * @param {string} prompt - 提示词\n * @param {Object} options - 选项\n */\n async complete(prompt, options = {}) {\n try {\n const response = await this.client.post(\"/completions\", {\n model: options.model || \"gpt-3.5-turbo-instruct\",\n prompt,\n temperature: options.temperature || 0.7,\n max_tokens: options.max_tokens || 1000,\n top_p: options.top_p || 1,\n stream: false,\n });\n\n const choice = response.data.choices[0];\n\n return {\n text: choice.text,\n finish_reason: choice.finish_reason,\n model: response.data.model,\n usage: response.data.usage,\n tokens: response.data.usage?.total_tokens || 0,\n };\n } catch (error) {\n logger.error(\"[OpenAIClient] 补全失败:\", error.response?.data || error);\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 生成嵌入向量\n * @param {string|Array} input - 文本或文本数组\n * @param {string} model - 模型(可选,默认使用配置的embeddingModel)\n */\n async embeddings(input, model = null) {\n try {\n const embeddingModel = model || this.embeddingModel;\n\n const response = await this.client.post(\"/embeddings\", {\n model: embeddingModel,\n input,\n });\n\n if (Array.isArray(input)) {\n return response.data.data.map((d) => d.embedding);\n } else {\n return response.data.data[0].embedding;\n }\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 生成嵌入失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 列出可用模型\n */\n async listModels() {\n try {\n const response = await this.client.get(\"/models\");\n return response.data.data;\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 列出模型失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 获取模型信息\n * @param {string} modelId - 模型ID\n */\n async getModel(modelId) {\n try {\n const response = await this.client.get(`/models/${modelId}`);\n return response.data;\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 获取模型信息失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 格式化 API 错误为用户友好的消息\n * @param {Error} error - axios 错误对象\n * @returns {string} 用户友好的错误消息\n */\n _formatAPIError(error) {\n const status = error.response?.status;\n const serverMessage = error.response?.data?.error?.message;\n const baseURL = this.baseURL || \"\";\n\n switch (status) {\n case 401:\n return `API 密钥无效或已过期,请在设置中检查 API Key 配置(${baseURL})`;\n case 403:\n return `API 访问被拒绝,请检查 API Key 权限或账户状态(${baseURL})`;\n case 429:\n return `API 请求频率超限或额度用尽,请稍后重试或检查账户余额(${baseURL})`;\n case 500:\n case 502:\n case 503:\n return `API 服务暂时不可用(HTTP ${status}),请稍后重试(${baseURL})`;\n case 404:\n return `API 端点不存在或模型不可用,请检查 API 地址和模型配置(${baseURL})`;\n default:\n if (\n error.code === \"ECONNABORTED\" ||\n error.message?.includes(\"timeout\")\n ) {\n return `API 请求超时,请检查网络连接或稍后重试(${baseURL})`;\n }\n if (error.code === \"ECONNREFUSED\") {\n return `无法连接到 API 服务,请检查服务地址是否正确(${baseURL})`;\n }\n if (error.code === \"ENOTFOUND\") {\n return `无法解析 API 服务地址,请检查网络连接和 API 地址配置(${baseURL})`;\n }\n return serverMessage || error.message;\n }\n }\n}\n\n/**\n * DeepSeek 客户端 (使用OpenAI兼容API)\n */\nclass DeepSeekClient extends OpenAIClient {\n constructor(config = {}) {\n super({\n ...config,\n baseURL: config.baseURL || \"https://api.deepseek.com/v1\",\n model: config.model || \"deepseek-chat\",\n });\n }\n}\n\nmodule.exports = {\n OpenAIClient,\n DeepSeekClient,\n};\n"}, "3b0b2dc09dceaf104b65bdb7bdc4448fc55e53a5": {"t": 1785754071.420102, "n": 12132, "h": "fda3dd86dfa25bed", "c": "/**\n * OpenAI 兼容 API 客户端\n *\n * 支持: OpenAI, DeepSeek, 以及其他兼容OpenAI API的服务\n */\n\nconst { logger } = require(\"../utils/logger.js\");\nconst axios = require(\"axios\");\nconst EventEmitter = require(\"events\");\n\n/**\n * OpenAI兼容客户端类\n */\nclass OpenAIClient extends EventEmitter {\n constructor(config = {}) {\n super();\n\n // API配置\n this.apiKey = config.apiKey;\n this.baseURL = config.baseURL || \"https://api.openai.com/v1\";\n this.model = config.model || \"gpt-3.5-turbo\";\n this.embeddingModel = config.embeddingModel || \"text-embedding-ada-002\";\n this.timeout = config.timeout || 300000; // 5 minutes default\n this.maxRetries = config.maxRetries || 2; // Retry up to 2 times on timeout\n this.organization = config.organization;\n\n // 创建axios实例\n this.client = axios.create({\n baseURL: this.baseURL,\n timeout: this.timeout,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),\n ...(this.organization && { \"OpenAI-Organization\": this.organization }),\n },\n });\n }\n\n /**\n * 检查服务状态\n */\n async checkStatus() {\n try {\n // 火山引擎特殊处理:使用简单的聊天测试代替模型列表\n if (this.baseURL && this.baseURL.includes(\"volces.com\")) {\n logger.info(\"[OpenAIClient] 检测到火山引擎,使用聊天测试代替模型列表\");\n\n const testResponse = await this.client.post(\n \"/chat/completions\",\n {\n model: this.model,\n messages: [{ role: \"user\", content: \"hi\" }],\n max_tokens: 5,\n },\n {\n timeout: 10000, // 10秒快速超时\n },\n );\n\n return {\n available: true,\n models: [\n {\n name: this.model,\n created: Date.now(),\n owned_by: \"volcengine\",\n },\n ],\n testResponse: testResponse.data,\n };\n }\n\n // 标准OpenAI API:尝试列出模型\n const response = await this.client.get(\"/models\");\n\n const models = response.data.data || [];\n\n return {\n available: true,\n models: models.map((m) => ({\n name: m.id,\n created: m.created,\n owned_by: m.owned_by,\n })),\n };\n } catch (error) {\n return {\n available: false,\n error: error.response?.data?.error?.message || error.message,\n models: [],\n };\n }\n }\n\n /**\n * 聊天补全(非流式)\n * @param {Array} messages - 消息数组\n * @param {Object} options - 选项\n */\n async chat(messages, options = {}) {\n const maxRetries = options.maxRetries ?? this.maxRetries;\n let lastError = null;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n if (attempt > 0) {\n logger.info(`[OpenAIClient] 重试第 ${attempt} 次...`);\n }\n\n // 构建请求体\n const requestBody = {\n model: options.model || this.model,\n messages,\n temperature: options.temperature || 0.7,\n top_p: options.top_p || 1,\n max_tokens: options.max_tokens,\n presence_penalty: options.presence_penalty || 0,\n frequency_penalty: options.frequency_penalty || 0,\n stream: false,\n };\n\n // 🔥 修复:只有在 tools 有效且非空时才添加(避免阿里云等API报错)\n if (\n options.tools &&\n Array.isArray(options.tools) &&\n options.tools.length > 0\n ) {\n // 验证每个tool都有必要的字段\n const validTools = options.tools.filter((tool) => {\n if (tool.type === \"function\") {\n return tool.function && tool.function.name;\n }\n return true; // 其他类型的工具暂时允许\n });\n\n if (validTools.length > 0) {\n requestBody.tools = validTools;\n }\n }\n\n const response = await this.client.post(\n \"/chat/completions\",\n requestBody,\n {\n ...(options.signal && { signal: options.signal }),\n },\n );\n\n const choice = response.data.choices[0];\n\n return {\n message: choice.message,\n finish_reason: choice.finish_reason,\n model: response.data.model,\n usage: response.data.usage,\n tokens: response.data.usage.total_tokens,\n };\n } catch (error) {\n lastError = error;\n const isTimeout =\n error.code === \"ECONNABORTED\" || error.message?.includes(\"timeout\");\n const isNetworkError =\n error.code === \"ECONNRESET\" || error.code === \"ENOTFOUND\";\n\n // Only retry on timeout or network errors\n if ((isTimeout || isNetworkError) && attempt < maxRetries) {\n logger.warn(\n `[OpenAIClient] 请求超时或网络错误,将重试: ${error.message}`,\n );\n // Wait before retry (exponential backoff: 2s, 4s)\n await new Promise((resolve) =>\n setTimeout(resolve, 2000 * Math.pow(2, attempt)),\n );\n continue;\n }\n\n logger.error(\"[OpenAIClient] 聊天失败:\", error.response?.data || error);\n throw new Error(this._formatAPIError(error));\n }\n }\n\n // Should not reach here, but just in case\n throw lastError;\n }\n\n /**\n * 聊天补全(流式)\n * @param {Array} messages - 消息数组\n * @param {Function} onChunk - 回调函数\n * @param {Object} options - 选项\n */\n async chatStream(messages, onChunk, options = {}) {\n try {\n // 构建请求体\n const requestBody = {\n model: options.model || this.model,\n messages,\n temperature: options.temperature || 0.7,\n top_p: options.top_p || 1,\n max_tokens: options.max_tokens,\n presence_penalty: options.presence_penalty || 0,\n frequency_penalty: options.frequency_penalty || 0,\n stream: true,\n };\n\n // 🔥 修复:只有在 tools 有效且非空时才添加(避免阿里云等API报错)\n if (\n options.tools &&\n Array.isArray(options.tools) &&\n options.tools.length > 0\n ) {\n // 验证每个tool都有必要的字段\n const validTools = options.tools.filter((tool) => {\n if (tool.type === \"function\") {\n return tool.function && tool.function.name;\n }\n return true; // 其他类型的工具暂时允许\n });\n\n if (validTools.length > 0) {\n requestBody.tools = validTools;\n }\n }\n\n const response = await this.client.post(\n \"/chat/completions\",\n requestBody,\n {\n responseType: \"stream\",\n },\n );\n\n const fullMessage = {\n role: \"assistant\",\n content: \"\",\n };\n\n return new Promise((resolve, reject) => {\n response.data.on(\"data\", (chunk) => {\n const lines = chunk\n .toString()\n .split(\"\\n\")\n .filter((line) => line.trim().startsWith(\"data: \"));\n\n for (const line of lines) {\n const data = line.replace(/^data: /, \"\");\n\n if (data === \"[DONE]\") {\n resolve({\n message: fullMessage,\n model: options.model || this.model,\n finish_reason: \"stop\",\n });\n return;\n }\n\n try {\n const parsed = JSON.parse(data);\n const delta = parsed.choices[0]?.delta;\n\n if (delta?.content) {\n fullMessage.content += delta.content;\n // 传递对象格式的chunk,保持统一接口\n onChunk({\n content: delta.content,\n delta: delta,\n fullContent: fullMessage.content,\n });\n }\n\n if (parsed.choices[0]?.finish_reason) {\n resolve({\n message: fullMessage,\n model: parsed.model,\n finish_reason: parsed.choices[0].finish_reason,\n });\n }\n } catch (e) {\n // 忽略解析错误\n }\n }\n });\n\n response.data.on(\"error\", (error) => {\n reject(error);\n });\n\n response.data.on(\"end\", () => {\n // 如果没有收到[DONE],也要resolve\n resolve({\n message: fullMessage,\n model: options.model || this.model,\n finish_reason: \"stop\",\n });\n });\n });\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 流式聊天失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 文本补全(非流式)- 兼容老版API\n * @param {string} prompt - 提示词\n * @param {Object} options - 选项\n */\n async complete(prompt, options = {}) {\n try {\n const response = await this.client.post(\"/completions\", {\n model: options.model || \"gpt-3.5-turbo-instruct\",\n prompt,\n temperature: options.temperature || 0.7,\n max_tokens: options.max_tokens || 1000,\n top_p: options.top_p || 1,\n stream: false,\n });\n\n const choice = response.data.choices[0];\n\n return {\n text: choice.text,\n finish_reason: choice.finish_reason,\n model: response.data.model,\n usage: response.data.usage,\n tokens: response.data.usage.total_tokens,\n };\n } catch (error) {\n logger.error(\"[OpenAIClient] 补全失败:\", error.response?.data || error);\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 生成嵌入向量\n * @param {string|Array} input - 文本或文本数组\n * @param {string} model - 模型(可选,默认使用配置的embeddingModel)\n */\n async embeddings(input, model = null) {\n try {\n const embeddingModel = model || this.embeddingModel;\n\n const response = await this.client.post(\"/embeddings\", {\n model: embeddingModel,\n input,\n });\n\n if (Array.isArray(input)) {\n return response.data.data.map((d) => d.embedding);\n } else {\n return response.data.data[0].embedding;\n }\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 生成嵌入失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 列出可用模型\n */\n async listModels() {\n try {\n const response = await this.client.get(\"/models\");\n return response.data.data;\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 列出模型失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 获取模型信息\n * @param {string} modelId - 模型ID\n */\n async getModel(modelId) {\n try {\n const response = await this.client.get(`/models/${modelId}`);\n return response.data;\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 获取模型信息失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 格式化 API 错误为用户友好的消息\n * @param {Error} error - axios 错误对象\n * @returns {string} 用户友好的错误消息\n */\n _formatAPIError(error) {\n const status = error.response?.status;\n const serverMessage = error.response?.data?.error?.message;\n const baseURL = this.baseURL || '';\n\n switch (status) {\n case 401:\n return `API 密钥无效或已过期,请在设置中检查 API Key 配置(${baseURL})`;\n case 403:\n return `API 访问被拒绝,请检查 API Key 权限或账户状态(${baseURL})`;\n case 429:\n return `API 请求频率超限或额度用尽,请稍后重试或检查账户余额(${baseURL})`;\n case 500:\n case 502:\n case 503:\n return `API 服务暂时不可用(HTTP ${status}),请稍后重试(${baseURL})`;\n case 404:\n return `API 端点不存在或模型不可用,请检查 API 地址和模型配置(${baseURL})`;\n default:\n if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {\n return `API 请求超时,请检查网络连接或稍后重试(${baseURL})`;\n }\n if (error.code === 'ECONNREFUSED') {\n return `无法连接到 API 服务,请检查服务地址是否正确(${baseURL})`;\n }\n if (error.code === 'ENOTFOUND') {\n return `无法解析 API 服务地址,请检查网络连接和 API 地址配置(${baseURL})`;\n }\n return serverMessage || error.message;\n }\n }\n}\n\n/**\n * DeepSeek 客户端 (使用OpenAI兼容API)\n */\nclass DeepSeekClient extends OpenAIClient {\n constructor(config = {}) {\n super({\n ...config,\n baseURL: config.baseURL || \"https://api.deepseek.com/v1\",\n model: config.model || \"deepseek-chat\",\n });\n }\n}\n\nmodule.exports = {\n OpenAIClient,\n DeepSeekClient,\n};\n"}, "c3fe912fefd60bb6332748f59241fb6de61e9c5b": {"t": 1785754073.875181, "n": 12132, "h": "fda3dd86dfa25bed", "c": "/**\n * OpenAI 兼容 API 客户端\n *\n * 支持: OpenAI, DeepSeek, 以及其他兼容OpenAI API的服务\n */\n\nconst { logger } = require(\"../utils/logger.js\");\nconst axios = require(\"axios\");\nconst EventEmitter = require(\"events\");\n\n/**\n * OpenAI兼容客户端类\n */\nclass OpenAIClient extends EventEmitter {\n constructor(config = {}) {\n super();\n\n // API配置\n this.apiKey = config.apiKey;\n this.baseURL = config.baseURL || \"https://api.openai.com/v1\";\n this.model = config.model || \"gpt-3.5-turbo\";\n this.embeddingModel = config.embeddingModel || \"text-embedding-ada-002\";\n this.timeout = config.timeout || 300000; // 5 minutes default\n this.maxRetries = config.maxRetries || 2; // Retry up to 2 times on timeout\n this.organization = config.organization;\n\n // 创建axios实例\n this.client = axios.create({\n baseURL: this.baseURL,\n timeout: this.timeout,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),\n ...(this.organization && { \"OpenAI-Organization\": this.organization }),\n },\n });\n }\n\n /**\n * 检查服务状态\n */\n async checkStatus() {\n try {\n // 火山引擎特殊处理:使用简单的聊天测试代替模型列表\n if (this.baseURL && this.baseURL.includes(\"volces.com\")) {\n logger.info(\"[OpenAIClient] 检测到火山引擎,使用聊天测试代替模型列表\");\n\n const testResponse = await this.client.post(\n \"/chat/completions\",\n {\n model: this.model,\n messages: [{ role: \"user\", content: \"hi\" }],\n max_tokens: 5,\n },\n {\n timeout: 10000, // 10秒快速超时\n },\n );\n\n return {\n available: true,\n models: [\n {\n name: this.model,\n created: Date.now(),\n owned_by: \"volcengine\",\n },\n ],\n testResponse: testResponse.data,\n };\n }\n\n // 标准OpenAI API:尝试列出模型\n const response = await this.client.get(\"/models\");\n\n const models = response.data.data || [];\n\n return {\n available: true,\n models: models.map((m) => ({\n name: m.id,\n created: m.created,\n owned_by: m.owned_by,\n })),\n };\n } catch (error) {\n return {\n available: false,\n error: error.response?.data?.error?.message || error.message,\n models: [],\n };\n }\n }\n\n /**\n * 聊天补全(非流式)\n * @param {Array} messages - 消息数组\n * @param {Object} options - 选项\n */\n async chat(messages, options = {}) {\n const maxRetries = options.maxRetries ?? this.maxRetries;\n let lastError = null;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n if (attempt > 0) {\n logger.info(`[OpenAIClient] 重试第 ${attempt} 次...`);\n }\n\n // 构建请求体\n const requestBody = {\n model: options.model || this.model,\n messages,\n temperature: options.temperature || 0.7,\n top_p: options.top_p || 1,\n max_tokens: options.max_tokens,\n presence_penalty: options.presence_penalty || 0,\n frequency_penalty: options.frequency_penalty || 0,\n stream: false,\n };\n\n // 🔥 修复:只有在 tools 有效且非空时才添加(避免阿里云等API报错)\n if (\n options.tools &&\n Array.isArray(options.tools) &&\n options.tools.length > 0\n ) {\n // 验证每个tool都有必要的字段\n const validTools = options.tools.filter((tool) => {\n if (tool.type === \"function\") {\n return tool.function && tool.function.name;\n }\n return true; // 其他类型的工具暂时允许\n });\n\n if (validTools.length > 0) {\n requestBody.tools = validTools;\n }\n }\n\n const response = await this.client.post(\n \"/chat/completions\",\n requestBody,\n {\n ...(options.signal && { signal: options.signal }),\n },\n );\n\n const choice = response.data.choices[0];\n\n return {\n message: choice.message,\n finish_reason: choice.finish_reason,\n model: response.data.model,\n usage: response.data.usage,\n tokens: response.data.usage.total_tokens,\n };\n } catch (error) {\n lastError = error;\n const isTimeout =\n error.code === \"ECONNABORTED\" || error.message?.includes(\"timeout\");\n const isNetworkError =\n error.code === \"ECONNRESET\" || error.code === \"ENOTFOUND\";\n\n // Only retry on timeout or network errors\n if ((isTimeout || isNetworkError) && attempt < maxRetries) {\n logger.warn(\n `[OpenAIClient] 请求超时或网络错误,将重试: ${error.message}`,\n );\n // Wait before retry (exponential backoff: 2s, 4s)\n await new Promise((resolve) =>\n setTimeout(resolve, 2000 * Math.pow(2, attempt)),\n );\n continue;\n }\n\n logger.error(\"[OpenAIClient] 聊天失败:\", error.response?.data || error);\n throw new Error(this._formatAPIError(error));\n }\n }\n\n // Should not reach here, but just in case\n throw lastError;\n }\n\n /**\n * 聊天补全(流式)\n * @param {Array} messages - 消息数组\n * @param {Function} onChunk - 回调函数\n * @param {Object} options - 选项\n */\n async chatStream(messages, onChunk, options = {}) {\n try {\n // 构建请求体\n const requestBody = {\n model: options.model || this.model,\n messages,\n temperature: options.temperature || 0.7,\n top_p: options.top_p || 1,\n max_tokens: options.max_tokens,\n presence_penalty: options.presence_penalty || 0,\n frequency_penalty: options.frequency_penalty || 0,\n stream: true,\n };\n\n // 🔥 修复:只有在 tools 有效且非空时才添加(避免阿里云等API报错)\n if (\n options.tools &&\n Array.isArray(options.tools) &&\n options.tools.length > 0\n ) {\n // 验证每个tool都有必要的字段\n const validTools = options.tools.filter((tool) => {\n if (tool.type === \"function\") {\n return tool.function && tool.function.name;\n }\n return true; // 其他类型的工具暂时允许\n });\n\n if (validTools.length > 0) {\n requestBody.tools = validTools;\n }\n }\n\n const response = await this.client.post(\n \"/chat/completions\",\n requestBody,\n {\n responseType: \"stream\",\n },\n );\n\n const fullMessage = {\n role: \"assistant\",\n content: \"\",\n };\n\n return new Promise((resolve, reject) => {\n response.data.on(\"data\", (chunk) => {\n const lines = chunk\n .toString()\n .split(\"\\n\")\n .filter((line) => line.trim().startsWith(\"data: \"));\n\n for (const line of lines) {\n const data = line.replace(/^data: /, \"\");\n\n if (data === \"[DONE]\") {\n resolve({\n message: fullMessage,\n model: options.model || this.model,\n finish_reason: \"stop\",\n });\n return;\n }\n\n try {\n const parsed = JSON.parse(data);\n const delta = parsed.choices[0]?.delta;\n\n if (delta?.content) {\n fullMessage.content += delta.content;\n // 传递对象格式的chunk,保持统一接口\n onChunk({\n content: delta.content,\n delta: delta,\n fullContent: fullMessage.content,\n });\n }\n\n if (parsed.choices[0]?.finish_reason) {\n resolve({\n message: fullMessage,\n model: parsed.model,\n finish_reason: parsed.choices[0].finish_reason,\n });\n }\n } catch (e) {\n // 忽略解析错误\n }\n }\n });\n\n response.data.on(\"error\", (error) => {\n reject(error);\n });\n\n response.data.on(\"end\", () => {\n // 如果没有收到[DONE],也要resolve\n resolve({\n message: fullMessage,\n model: options.model || this.model,\n finish_reason: \"stop\",\n });\n });\n });\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 流式聊天失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 文本补全(非流式)- 兼容老版API\n * @param {string} prompt - 提示词\n * @param {Object} options - 选项\n */\n async complete(prompt, options = {}) {\n try {\n const response = await this.client.post(\"/completions\", {\n model: options.model || \"gpt-3.5-turbo-instruct\",\n prompt,\n temperature: options.temperature || 0.7,\n max_tokens: options.max_tokens || 1000,\n top_p: options.top_p || 1,\n stream: false,\n });\n\n const choice = response.data.choices[0];\n\n return {\n text: choice.text,\n finish_reason: choice.finish_reason,\n model: response.data.model,\n usage: response.data.usage,\n tokens: response.data.usage.total_tokens,\n };\n } catch (error) {\n logger.error(\"[OpenAIClient] 补全失败:\", error.response?.data || error);\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 生成嵌入向量\n * @param {string|Array} input - 文本或文本数组\n * @param {string} model - 模型(可选,默认使用配置的embeddingModel)\n */\n async embeddings(input, model = null) {\n try {\n const embeddingModel = model || this.embeddingModel;\n\n const response = await this.client.post(\"/embeddings\", {\n model: embeddingModel,\n input,\n });\n\n if (Array.isArray(input)) {\n return response.data.data.map((d) => d.embedding);\n } else {\n return response.data.data[0].embedding;\n }\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 生成嵌入失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 列出可用模型\n */\n async listModels() {\n try {\n const response = await this.client.get(\"/models\");\n return response.data.data;\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 列出模型失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 获取模型信息\n * @param {string} modelId - 模型ID\n */\n async getModel(modelId) {\n try {\n const response = await this.client.get(`/models/${modelId}`);\n return response.data;\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 获取模型信息失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 格式化 API 错误为用户友好的消息\n * @param {Error} error - axios 错误对象\n * @returns {string} 用户友好的错误消息\n */\n _formatAPIError(error) {\n const status = error.response?.status;\n const serverMessage = error.response?.data?.error?.message;\n const baseURL = this.baseURL || '';\n\n switch (status) {\n case 401:\n return `API 密钥无效或已过期,请在设置中检查 API Key 配置(${baseURL})`;\n case 403:\n return `API 访问被拒绝,请检查 API Key 权限或账户状态(${baseURL})`;\n case 429:\n return `API 请求频率超限或额度用尽,请稍后重试或检查账户余额(${baseURL})`;\n case 500:\n case 502:\n case 503:\n return `API 服务暂时不可用(HTTP ${status}),请稍后重试(${baseURL})`;\n case 404:\n return `API 端点不存在或模型不可用,请检查 API 地址和模型配置(${baseURL})`;\n default:\n if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {\n return `API 请求超时,请检查网络连接或稍后重试(${baseURL})`;\n }\n if (error.code === 'ECONNREFUSED') {\n return `无法连接到 API 服务,请检查服务地址是否正确(${baseURL})`;\n }\n if (error.code === 'ENOTFOUND') {\n return `无法解析 API 服务地址,请检查网络连接和 API 地址配置(${baseURL})`;\n }\n return serverMessage || error.message;\n }\n }\n}\n\n/**\n * DeepSeek 客户端 (使用OpenAI兼容API)\n */\nclass DeepSeekClient extends OpenAIClient {\n constructor(config = {}) {\n super({\n ...config,\n baseURL: config.baseURL || \"https://api.deepseek.com/v1\",\n model: config.model || \"deepseek-chat\",\n });\n }\n}\n\nmodule.exports = {\n OpenAIClient,\n DeepSeekClient,\n};\n"}, "e2612b2f15916b59ffdb3c21a308f1a0e97bb940": {"t": 1785754076.2673094, "n": 12132, "h": "fda3dd86dfa25bed", "c": "/**\n * OpenAI 兼容 API 客户端\n *\n * 支持: OpenAI, DeepSeek, 以及其他兼容OpenAI API的服务\n */\n\nconst { logger } = require(\"../utils/logger.js\");\nconst axios = require(\"axios\");\nconst EventEmitter = require(\"events\");\n\n/**\n * OpenAI兼容客户端类\n */\nclass OpenAIClient extends EventEmitter {\n constructor(config = {}) {\n super();\n\n // API配置\n this.apiKey = config.apiKey;\n this.baseURL = config.baseURL || \"https://api.openai.com/v1\";\n this.model = config.model || \"gpt-3.5-turbo\";\n this.embeddingModel = config.embeddingModel || \"text-embedding-ada-002\";\n this.timeout = config.timeout || 300000; // 5 minutes default\n this.maxRetries = config.maxRetries || 2; // Retry up to 2 times on timeout\n this.organization = config.organization;\n\n // 创建axios实例\n this.client = axios.create({\n baseURL: this.baseURL,\n timeout: this.timeout,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),\n ...(this.organization && { \"OpenAI-Organization\": this.organization }),\n },\n });\n }\n\n /**\n * 检查服务状态\n */\n async checkStatus() {\n try {\n // 火山引擎特殊处理:使用简单的聊天测试代替模型列表\n if (this.baseURL && this.baseURL.includes(\"volces.com\")) {\n logger.info(\"[OpenAIClient] 检测到火山引擎,使用聊天测试代替模型列表\");\n\n const testResponse = await this.client.post(\n \"/chat/completions\",\n {\n model: this.model,\n messages: [{ role: \"user\", content: \"hi\" }],\n max_tokens: 5,\n },\n {\n timeout: 10000, // 10秒快速超时\n },\n );\n\n return {\n available: true,\n models: [\n {\n name: this.model,\n created: Date.now(),\n owned_by: \"volcengine\",\n },\n ],\n testResponse: testResponse.data,\n };\n }\n\n // 标准OpenAI API:尝试列出模型\n const response = await this.client.get(\"/models\");\n\n const models = response.data.data || [];\n\n return {\n available: true,\n models: models.map((m) => ({\n name: m.id,\n created: m.created,\n owned_by: m.owned_by,\n })),\n };\n } catch (error) {\n return {\n available: false,\n error: error.response?.data?.error?.message || error.message,\n models: [],\n };\n }\n }\n\n /**\n * 聊天补全(非流式)\n * @param {Array} messages - 消息数组\n * @param {Object} options - 选项\n */\n async chat(messages, options = {}) {\n const maxRetries = options.maxRetries ?? this.maxRetries;\n let lastError = null;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n if (attempt > 0) {\n logger.info(`[OpenAIClient] 重试第 ${attempt} 次...`);\n }\n\n // 构建请求体\n const requestBody = {\n model: options.model || this.model,\n messages,\n temperature: options.temperature || 0.7,\n top_p: options.top_p || 1,\n max_tokens: options.max_tokens,\n presence_penalty: options.presence_penalty || 0,\n frequency_penalty: options.frequency_penalty || 0,\n stream: false,\n };\n\n // 🔥 修复:只有在 tools 有效且非空时才添加(避免阿里云等API报错)\n if (\n options.tools &&\n Array.isArray(options.tools) &&\n options.tools.length > 0\n ) {\n // 验证每个tool都有必要的字段\n const validTools = options.tools.filter((tool) => {\n if (tool.type === \"function\") {\n return tool.function && tool.function.name;\n }\n return true; // 其他类型的工具暂时允许\n });\n\n if (validTools.length > 0) {\n requestBody.tools = validTools;\n }\n }\n\n const response = await this.client.post(\n \"/chat/completions\",\n requestBody,\n {\n ...(options.signal && { signal: options.signal }),\n },\n );\n\n const choice = response.data.choices[0];\n\n return {\n message: choice.message,\n finish_reason: choice.finish_reason,\n model: response.data.model,\n usage: response.data.usage,\n tokens: response.data.usage.total_tokens,\n };\n } catch (error) {\n lastError = error;\n const isTimeout =\n error.code === \"ECONNABORTED\" || error.message?.includes(\"timeout\");\n const isNetworkError =\n error.code === \"ECONNRESET\" || error.code === \"ENOTFOUND\";\n\n // Only retry on timeout or network errors\n if ((isTimeout || isNetworkError) && attempt < maxRetries) {\n logger.warn(\n `[OpenAIClient] 请求超时或网络错误,将重试: ${error.message}`,\n );\n // Wait before retry (exponential backoff: 2s, 4s)\n await new Promise((resolve) =>\n setTimeout(resolve, 2000 * Math.pow(2, attempt)),\n );\n continue;\n }\n\n logger.error(\"[OpenAIClient] 聊天失败:\", error.response?.data || error);\n throw new Error(this._formatAPIError(error));\n }\n }\n\n // Should not reach here, but just in case\n throw lastError;\n }\n\n /**\n * 聊天补全(流式)\n * @param {Array} messages - 消息数组\n * @param {Function} onChunk - 回调函数\n * @param {Object} options - 选项\n */\n async chatStream(messages, onChunk, options = {}) {\n try {\n // 构建请求体\n const requestBody = {\n model: options.model || this.model,\n messages,\n temperature: options.temperature || 0.7,\n top_p: options.top_p || 1,\n max_tokens: options.max_tokens,\n presence_penalty: options.presence_penalty || 0,\n frequency_penalty: options.frequency_penalty || 0,\n stream: true,\n };\n\n // 🔥 修复:只有在 tools 有效且非空时才添加(避免阿里云等API报错)\n if (\n options.tools &&\n Array.isArray(options.tools) &&\n options.tools.length > 0\n ) {\n // 验证每个tool都有必要的字段\n const validTools = options.tools.filter((tool) => {\n if (tool.type === \"function\") {\n return tool.function && tool.function.name;\n }\n return true; // 其他类型的工具暂时允许\n });\n\n if (validTools.length > 0) {\n requestBody.tools = validTools;\n }\n }\n\n const response = await this.client.post(\n \"/chat/completions\",\n requestBody,\n {\n responseType: \"stream\",\n },\n );\n\n const fullMessage = {\n role: \"assistant\",\n content: \"\",\n };\n\n return new Promise((resolve, reject) => {\n response.data.on(\"data\", (chunk) => {\n const lines = chunk\n .toString()\n .split(\"\\n\")\n .filter((line) => line.trim().startsWith(\"data: \"));\n\n for (const line of lines) {\n const data = line.replace(/^data: /, \"\");\n\n if (data === \"[DONE]\") {\n resolve({\n message: fullMessage,\n model: options.model || this.model,\n finish_reason: \"stop\",\n });\n return;\n }\n\n try {\n const parsed = JSON.parse(data);\n const delta = parsed.choices[0]?.delta;\n\n if (delta?.content) {\n fullMessage.content += delta.content;\n // 传递对象格式的chunk,保持统一接口\n onChunk({\n content: delta.content,\n delta: delta,\n fullContent: fullMessage.content,\n });\n }\n\n if (parsed.choices[0]?.finish_reason) {\n resolve({\n message: fullMessage,\n model: parsed.model,\n finish_reason: parsed.choices[0].finish_reason,\n });\n }\n } catch (e) {\n // 忽略解析错误\n }\n }\n });\n\n response.data.on(\"error\", (error) => {\n reject(error);\n });\n\n response.data.on(\"end\", () => {\n // 如果没有收到[DONE],也要resolve\n resolve({\n message: fullMessage,\n model: options.model || this.model,\n finish_reason: \"stop\",\n });\n });\n });\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 流式聊天失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 文本补全(非流式)- 兼容老版API\n * @param {string} prompt - 提示词\n * @param {Object} options - 选项\n */\n async complete(prompt, options = {}) {\n try {\n const response = await this.client.post(\"/completions\", {\n model: options.model || \"gpt-3.5-turbo-instruct\",\n prompt,\n temperature: options.temperature || 0.7,\n max_tokens: options.max_tokens || 1000,\n top_p: options.top_p || 1,\n stream: false,\n });\n\n const choice = response.data.choices[0];\n\n return {\n text: choice.text,\n finish_reason: choice.finish_reason,\n model: response.data.model,\n usage: response.data.usage,\n tokens: response.data.usage.total_tokens,\n };\n } catch (error) {\n logger.error(\"[OpenAIClient] 补全失败:\", error.response?.data || error);\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 生成嵌入向量\n * @param {string|Array} input - 文本或文本数组\n * @param {string} model - 模型(可选,默认使用配置的embeddingModel)\n */\n async embeddings(input, model = null) {\n try {\n const embeddingModel = model || this.embeddingModel;\n\n const response = await this.client.post(\"/embeddings\", {\n model: embeddingModel,\n input,\n });\n\n if (Array.isArray(input)) {\n return response.data.data.map((d) => d.embedding);\n } else {\n return response.data.data[0].embedding;\n }\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 生成嵌入失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 列出可用模型\n */\n async listModels() {\n try {\n const response = await this.client.get(\"/models\");\n return response.data.data;\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 列出模型失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 获取模型信息\n * @param {string} modelId - 模型ID\n */\n async getModel(modelId) {\n try {\n const response = await this.client.get(`/models/${modelId}`);\n return response.data;\n } catch (error) {\n logger.error(\n \"[OpenAIClient] 获取模型信息失败:\",\n error.response?.data || error,\n );\n throw new Error(this._formatAPIError(error));\n }\n }\n\n /**\n * 格式化 API 错误为用户友好的消息\n * @param {Error} error - axios 错误对象\n * @returns {string} 用户友好的错误消息\n */\n _formatAPIError(error) {\n const status = error.response?.status;\n const serverMessage = error.response?.data?.error?.message;\n const baseURL = this.baseURL || '';\n\n switch (status) {\n case 401:\n return `API 密钥无效或已过期,请在设置中检查 API Key 配置(${baseURL})`;\n case 403:\n return `API 访问被拒绝,请检查 API Key 权限或账户状态(${baseURL})`;\n case 429:\n return `API 请求频率超限或额度用尽,请稍后重试或检查账户余额(${baseURL})`;\n case 500:\n case 502:\n case 503:\n return `API 服务暂时不可用(HTTP ${status}),请稍后重试(${baseURL})`;\n case 404:\n return `API 端点不存在或模型不可用,请检查 API 地址和模型配置(${baseURL})`;\n default:\n if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {\n return `API 请求超时,请检查网络连接或稍后重试(${baseURL})`;\n }\n if (error.code === 'ECONNREFUSED') {\n return `无法连接到 API 服务,请检查服务地址是否正确(${baseURL})`;\n }\n if (error.code === 'ENOTFOUND') {\n return `无法解析 API 服务地址,请检查网络连接和 API 地址配置(${baseURL})`;\n }\n return serverMessage || error.message;\n }\n }\n}\n\n/**\n * DeepSeek 客户端 (使用OpenAI兼容API)\n */\nclass DeepSeekClient extends OpenAIClient {\n constructor(config = {}) {\n super({\n ...config,\n baseURL: config.baseURL || \"https://api.deepseek.com/v1\",\n model: config.model || \"deepseek-chat\",\n });\n }\n}\n\nmodule.exports = {\n OpenAIClient,\n DeepSeekClient,\n};\n"}}, "docs/cli/managed-agents.md": {"9e2a3238426499a3de1d228034e66dab91cbfa2c": {"t": 1785746552.5445771, "n": 68093, "h": "eef0b4133ceaa7d5", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n### 后台 Agent 会话\n\n长任务可以脱离当前终端运行:\n\n```bash\ncc agent --bg -p \"运行完整测试并修复失败\"\ncc agents background # 仅运行中\ncc agents background --all # 包含完成/失败/停止\ncc agents logs -n 200\ncc agents stop \n```\n\n后台 worker 与实际 Agent 进程独立,状态和日志保存在 `~/.chainlesschain/background-agents/`。状态文件不保存命令行参数,避免 `--api-key` 等凭据进入会话索引;启动 job 文件使用 `0600` 并由 worker 读取后立即删除。当前 `--bg` 不与 `--worktree` 同时使用,可以先进入目标 worktree 再启动后台 Agent。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | `CC_MAX_RETRIES`(原生)/ `CLAUDE_CODE_MAX_RETRIES`(平价),封顶 15 |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- **重试次数**:默认 2,可经 `CC_MAX_RETRIES` 或 `CLAUDE_CODE_MAX_RETRIES`(Claude-Code 2.1.186 平价)调节,**clamp 到 `[0, 15]`**(0=关重试;封顶防死端点变成多分钟指数退避挂起)。两值都设时 `CC_MAX_RETRIES` 胜;agent 与 chat 两路径同源。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir `(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Notebooks — `notebook_edit` + notebook-aware `read_file`(Claude-Code NotebookEdit/Read 平价)\n\nJupyter `.ipynb` 读写一对工具,补齐 cc agent 唯一缺的 Claude Code 工具。\n\n**读**:`read_file` 对 `.ipynb` 自动渲染成**紧凑单元清单**(每格 index + id + type + source,outputs 只摘要不 dump),而非原始 JSON——避免 base64 输出把上下文撑爆/被 50k 截断藏住单元。传 `raw:true` 取底层 JSON。\n\n```\nJupyter notebook — 2 cell(s), nbformat 4. Edit cells with the notebook_edit tool …\n\n── Cell 0 [code id=c1] ──\nx = 1\nprint(x)\n ⟨1 output(s) hidden⟩\n\n── Cell 1 [markdown id=m1] ──\n# Title\n```\n\n**写**:`notebook_edit` 改单个单元——\n\n| 参数 | 说明 |\n|---|---|\n| `path` | `.ipynb` 路径(必填) |\n| `edit_mode` | `replace`(默认)/ `insert` / `delete` |\n| `cell_id` / `cell_index` | 目标定位(cell_id 优先;从渲染清单里读 id) |\n| `cell_type` | `code` / `markdown`——`insert` 必填 |\n| `new_source` | 新单元源文本(replace/insert 必填) |\n\n- **replace**:覆盖目标格 source;若是 code 格,顺带清空 `outputs` + `execution_count`(旧输出会误导)。\n- **insert**:在目标格**之后**插新格(无目标→插到**顶部**),自动生成 cell id。\n- **delete**:移除目标格。\n- 源以保留行尾 `\\n` 的行数组存盘,1-space 缩进 + 尾换行重序列化(nbformat 习惯)。\n\n**典型流程**:`read_file notebook.ipynb`(看到各格 id)→ `notebook_edit {path, cell_id, new_source}`。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task`/`Agent` | `spawn_sub_agent` | 子 agent 类型 | `Agent(explorer)`、`Agent(executor,design)` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- **命名参数**(Claude-Code 2.1.178):`Tool(param:value)` 匹配任一具名实参,\n 如 `Bash(command:rm*)`、`mcp__db__query(table:users)`。\n- **子 agent 类型**(Claude-Code 2.1.186):`Agent(type)` / `Agent(typeA,typeB)`\n 按 `spawn_sub_agent` 调用的类型(`profile` / 具名 `agent` / `role`,取其一)匹配——\n 逗号列表任一命中即可、类型名大小写不敏感、支持 `*`/`?` glob。从 Claude-Code\n `settings.json` 直接移植的 `Agent(…)` 规则因此会被执行而非静默忽略。\n (`Task(profile:explorer)` 命名参数写法仍可用。)\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n### Managed Settings(组织强制策略)\n\n管理员可以部署不可被 user/project/local、`--settings` 或环境权限规则覆盖的策略文件:\n\n- Windows:`%ProgramData%\\ChainlessChain\\managed-settings.json`\n- Linux/macOS:`/etc/chainlesschain/managed-settings.json`\n- 测试或定制部署:`CC_MANAGED_SETTINGS=/absolute/path.json`\n\n```json\n{\n \"permissions\": {\n \"deny\": [\"Bash(curl:*)\", \"Read(./.env*)\"]\n },\n \"allowManagedPermissionRulesOnly\": true,\n \"allowManagedHooksOnly\": true,\n \"allowManagedMcpServersOnly\": true,\n \"allowedMcpServers\": [\"company-github\", \"company-jira\"],\n \"deniedMcpServers\": [\"untrusted-local\"],\n \"allowedPlugins\": [\"company-review\"],\n \"deniedPlugins\": [\"untrusted-plugin\"],\n \"allowedPluginSources\": [\"company-marketplace\"],\n \"blockedMarketplaces\": [\"unknown-public-source\"],\n \"requireSignedPlugins\": true,\n \"trustedPluginKeySha256\": [\"\"],\n \"requirePluginCapabilityDeclarations\": true,\n \"requirePluginCapabilityConsent\": true,\n \"disableBypassPermissionsMode\": \"disable\",\n \"model\": \"managed-model\",\n \"env\": { \"AUDIT_ENABLED\": \"1\" }\n}\n```\n\n`allowManagedPermissionRulesOnly` 会忽略所有非 managed 的 allow/ask/deny,并禁用 REPL 的 always-allow 持久化;`allowManagedHooksOnly` 禁止 user/project/local/`--settings` hooks,但 managed hooks 在 `--safe-mode` 下仍执行;MCP allow/deny 在连接和启动 stdio server 之前生效,覆盖临时配置、注册配置、项目 `.mcp.json` 和 IDE/PDH bridge。`disableBypassPermissionsMode` 会拒绝 `--permission-mode bypassPermissions`。managed 文件存在但无法读取或 JSON 损坏时 Agent 失败关闭。`cc permissions list --json` 会返回 `managed` 与 `managedFile`,文本模式也会标出强制策略。\n\n插件安装策略在写数据库和安装 skills 之前执行。`requireSignedPlugins` 要求提供 manifest、二进制 Ed25519 detached signature 和 PEM 公钥:\n\n```bash\ncc plugin install company-review \\\n --source company-marketplace \\\n --manifest .claude-plugin/plugin.json \\\n --sha256 \\\n --signature plugin.sig \\\n --public-key company-plugin-key.pem\n```\n\n`deniedPlugins` 优先于 allowlist;source/marketplace 策略要求安装方显式声明 `--source`。启用强制签名时还必须配置受信公钥的 SPKI DER SHA-256 指纹,不能用任意自签公钥绕过。manifest SHA-256、签名或公钥信任不匹配,以及 manifest JSON 损坏都会失败关闭。`requirePluginCapabilityDeclarations` 会拒绝没有显式 `permissions` block 的 legacy 插件;`requirePluginCapabilityConsent` 除要求当前 capability 集已授权外,也隐含声明强制,不能通过删除 `permissions` 绕过 consent。单机可分别使用 `CC_REQUIRE_PLUGIN_CAPABILITIES=1` 和 `CC_REQUIRE_PLUGIN_CONSENT=1` 收紧。插件直接 URL MCP transport 会在连接前校验目标 hostname 是否落在声明的 `network` domains 中;stdio 子进程的网络/文件边界仍由 Process Broker 平台沙箱负责。\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n**`/permissions denials`**(Claude-Code 2.1.193 \"recent denials\" 平价):回看本会话被**拦下**的工具调用——shell-policy / ApprovalGate 档位 / settings 规则 / hook 拦截。每条显示工具+尝试的参数摘要、命中的 `via`(+规则名)、相对时间、原因文本(最近在上,封顶 20 条,纯内存)。**只记策略拒绝,不记普通执行失败**(命令退非零 / 文件不存在等本就有 `error` 但无拦截标记)。主 `/permissions` 视图在有拦截时附一行提示导向它。连续重复的同一拦截会**折叠成 `×N`**(模型撞墙后常反复重试,避免挤掉其它不同的拦截)。\n\n**Headless 拦截汇总**:非交互运行(`cc agent -p`)在结束时把被拦的工具调用汇总输出,三种 `--output-format` 都覆盖——text 打到 **stderr**(`N tool call(s) were denied by policy this run:` + 同款列表);stream-json 在最终 `result` 事件前发一条 `denials_summary` 事件(`{count, denials[]}`);**json** 在 result envelope 里带一个 `denials` 数组(仅有拦截时出现 → 无拦截时 envelope 逐字节不变)。让 CI / 脚本能一眼/可解析地看出 agent 被什么策略挡了(等价 REPL 的 `/permissions denials`)。**模型侧**:每条策略拒绝回给模型的 tool result 也是**可操作的**——明说这是策略决定、重试无用、该让用户介入(减少撞墙重试空耗 turn/token)。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n### `autoMode.classifyAllShell`(Claude-Code 2.1.193 平价)\n\nshell-policy 默认给一组**验证类命令**(`npm test` / `npm run lint` / `npm run build` /\n单文件 Playwright / `rg`)走**快车道 ALLOW**(风险 LOW,自动放行不弹确认)。在\n`.claude/settings.json` 里设 `{ \"autoMode\": { \"classifyAllShell\": true } }` 后,这组内置\n白名单也被**降级为 WARN**(风险 MEDIUM)→ 经 ApprovalGate 确认,**任何 shell 命令都不再静默自跑**。\n\n- 只影响**内置便利白名单**;硬黑名单(`rm`/`curl`/destructive git/disk)仍 **DENY**,未分类命令本就是 WARN。\n- 用户显式的 settings `allow` 规则(`Bash(npm run test:*)`)是**另一层有意预授权**,不受此开关影响。\n- 层级与其它设置一致(`~/.claude` < 项目 < `.local` < `--settings`,最近层胜);未设 = 关。\n- 无 ApprovalGate 时(纯 headless 无 gate)降级为 WARN 仍是 allowed——开关的\"牙齿\"来自交互档位的确认。\n\n## Agent Shell OS Sandbox\n\n`cc agent --sandbox` 会把 `run_shell` 放入一次性 Docker 容器执行,而不只是检查命令字符串:\n\n```bash\ncc agent --sandbox -p \"运行测试并修复失败\"\ncc agent --sandbox node:22-alpine -p \"检查构建\"\ncc agent --sandbox --sandbox-network -p \"更新依赖并运行测试\"\n```\n\n默认挂载当前工作区为可写 `/workspace`、禁用容器网络,只传 Agent 会话标识环境变量,不传宿主 API key。Docker 缺失或容器启动失败时会失败关闭。首版不支持 sandbox 内后台 shell。\n\n也可通过分层 `.claude/settings.json` 启用并配置沙箱。数组策略跨 user/project/local/managed 层累加,标量由更高优先级覆盖:\n\n```json\n{\n \"sandbox\": {\n \"enabled\": true,\n \"engine\": \"bubblewrap\",\n \"failIfUnavailable\": true,\n \"allowUnsandboxedCommands\": false,\n \"filesystem\": {\n \"allowWrite\": [\"/tmp/build\"],\n \"denyRead\": [\"~/.ssh\", \".env\"]\n },\n \"network\": {\n \"allowedDomains\": [\"registry.npmjs.org\"]\n },\n \"excludedCommands\": [\"docker\"]\n }\n}\n```\n\n`bubblewrap` 后端在 Linux/WSL2 使用只读宿主根、可写工作区、独立 PID/IPC/UTS/user namespace,默认不共享网络。Docker 后端目前只保证工作区挂载与全局断网;若配置细粒度文件策略会失败关闭并提示改用 `bubblewrap`。域名级网络策略需要独立代理,代理尚未配置时也会拒绝开启不受限网络,避免产生“策略已生效”的假象。\n\n## Credential 读取保护(Claude-Code 2.1.189 `sandbox.credentials` 平价)\n\ncc 不做 OS 沙箱,但同样的意图在**工具层**落地:Agent 不应把用户的密钥静默吸进模型上下文\n(可能被回显、写进转录、或被后续工具外泄)。两条读取路径被**确认优先 / headless fail-closed**\n拦截(与 sensitive-file **写**保护同款):\n\n- **`read_file`** 指向凭据文件:`.env`(及 `.env.local` / `.env.production` 等真实变体——但\n `.env.example` / `.env.sample` / `.env.template` 等模板**不拦**)、`~/.aws/credentials`、\n `~/.kube/config`、`~/.docker/config.json`、`~/.config/gh/hosts.yml`、`.npmrc` / `.netrc` /\n `.git-credentials` / `.pgpass`、私钥与证书材料(`*.pem` / `*.key` / `*.pfx` / `*.p12` /\n `*.jks` / `id_rsa` 等;`*.pub` / `*.crt` 公钥**不拦**)、`secrets.{json,yaml}`。\n- **`run_shell`** 读凭据:用 `cat` / `type` / `Get-Content` / `head` … 读上述凭据文件;\n `echo $ANTHROPIC_API_KEY` / `printf \"%s\" \"$AWS_SECRET_ACCESS_KEY\"` / `echo %GITHUB_TOKEN%` /\n `Write-Host $env:DB_PASSWORD` / `printenv OPENAI_API_KEY` 这类**打印密钥型环境变量**;\n 以及 `printenv` / `env` / `Get-ChildItem env:` 这类**全量 env dump**。复合命令的**每一段**\n 都查(`echo hi && cat .env` 也会被拦)。仅**消费**密钥而不回显的命令(`mytool --token $API_TOKEN`)\n 不拦——值不会落进输出。\n\n旁路与开关:\n\n- **显式 settings `allow` 规则**(`Read(./**)` / `Bash(cat:*)` 等)或交互确认 `ask` 已通过 →\n 预授权,不再拦。\n- 交互模式弹确认(一次按键);**headless 无确认器 → fail-closed 拒绝**,报\n `[Credential Guard] … requires confirmation — denied`。\n- `CC_CREDENTIAL_GUARD=0`(/ `false` / `no` / `off`)整体关闭。\n- **有意分歧**:`--safe-mode` **不**关闭本保护——和权限 deny 规则一样,这是**安全面**而非\n 自定义项,\"裸跑排查自定义\"绝不能放宽 Agent 能读到的密钥。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `Bash,PowerShell` 逗号 / `*` 通配 / `/regex/`(管道与逗号可混用,空段忽略)。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "b682c763f38107e31bb46c9cdf97c3c41bdfc8a6": {"t": 1785754071.2889667, "n": 68093, "h": "eef0b4133ceaa7d5", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n### 后台 Agent 会话\n\n长任务可以脱离当前终端运行:\n\n```bash\ncc agent --bg -p \"运行完整测试并修复失败\"\ncc agents background # 仅运行中\ncc agents background --all # 包含完成/失败/停止\ncc agents logs -n 200\ncc agents stop \n```\n\n后台 worker 与实际 Agent 进程独立,状态和日志保存在 `~/.chainlesschain/background-agents/`。状态文件不保存命令行参数,避免 `--api-key` 等凭据进入会话索引;启动 job 文件使用 `0600` 并由 worker 读取后立即删除。当前 `--bg` 不与 `--worktree` 同时使用,可以先进入目标 worktree 再启动后台 Agent。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | `CC_MAX_RETRIES`(原生)/ `CLAUDE_CODE_MAX_RETRIES`(平价),封顶 15 |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- **重试次数**:默认 2,可经 `CC_MAX_RETRIES` 或 `CLAUDE_CODE_MAX_RETRIES`(Claude-Code 2.1.186 平价)调节,**clamp 到 `[0, 15]`**(0=关重试;封顶防死端点变成多分钟指数退避挂起)。两值都设时 `CC_MAX_RETRIES` 胜;agent 与 chat 两路径同源。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Notebooks — `notebook_edit` + notebook-aware `read_file`(Claude-Code NotebookEdit/Read 平价)\n\nJupyter `.ipynb` 读写一对工具,补齐 cc agent 唯一缺的 Claude Code 工具。\n\n**读**:`read_file` 对 `.ipynb` 自动渲染成**紧凑单元清单**(每格 index + id + type + source,outputs 只摘要不 dump),而非原始 JSON——避免 base64 输出把上下文撑爆/被 50k 截断藏住单元。传 `raw:true` 取底层 JSON。\n\n```\nJupyter notebook — 2 cell(s), nbformat 4. Edit cells with the notebook_edit tool …\n\n── Cell 0 [code id=c1] ──\nx = 1\nprint(x)\n ⟨1 output(s) hidden⟩\n\n── Cell 1 [markdown id=m1] ──\n# Title\n```\n\n**写**:`notebook_edit` 改单个单元——\n\n| 参数 | 说明 |\n|---|---|\n| `path` | `.ipynb` 路径(必填) |\n| `edit_mode` | `replace`(默认)/ `insert` / `delete` |\n| `cell_id` / `cell_index` | 目标定位(cell_id 优先;从渲染清单里读 id) |\n| `cell_type` | `code` / `markdown`——`insert` 必填 |\n| `new_source` | 新单元源文本(replace/insert 必填) |\n\n- **replace**:覆盖目标格 source;若是 code 格,顺带清空 `outputs` + `execution_count`(旧输出会误导)。\n- **insert**:在目标格**之后**插新格(无目标→插到**顶部**),自动生成 cell id。\n- **delete**:移除目标格。\n- 源以保留行尾 `\\n` 的行数组存盘,1-space 缩进 + 尾换行重序列化(nbformat 习惯)。\n\n**典型流程**:`read_file notebook.ipynb`(看到各格 id)→ `notebook_edit {path, cell_id, new_source}`。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task`/`Agent` | `spawn_sub_agent` | 子 agent 类型 | `Agent(explorer)`、`Agent(executor,design)` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- **命名参数**(Claude-Code 2.1.178):`Tool(param:value)` 匹配任一具名实参,\n 如 `Bash(command:rm*)`、`mcp__db__query(table:users)`。\n- **子 agent 类型**(Claude-Code 2.1.186):`Agent(type)` / `Agent(typeA,typeB)`\n 按 `spawn_sub_agent` 调用的类型(`profile` / 具名 `agent` / `role`,取其一)匹配——\n 逗号列表任一命中即可、类型名大小写不敏感、支持 `*`/`?` glob。从 Claude-Code\n `settings.json` 直接移植的 `Agent(…)` 规则因此会被执行而非静默忽略。\n (`Task(profile:explorer)` 命名参数写法仍可用。)\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n### Managed Settings(组织强制策略)\n\n管理员可以部署不可被 user/project/local、`--settings` 或环境权限规则覆盖的策略文件:\n\n- Windows:`%ProgramData%\\ChainlessChain\\managed-settings.json`\n- Linux/macOS:`/etc/chainlesschain/managed-settings.json`\n- 测试或定制部署:`CC_MANAGED_SETTINGS=/absolute/path.json`\n\n```json\n{\n \"permissions\": {\n \"deny\": [\"Bash(curl:*)\", \"Read(./.env*)\"]\n },\n \"allowManagedPermissionRulesOnly\": true,\n \"allowManagedHooksOnly\": true,\n \"allowManagedMcpServersOnly\": true,\n \"allowedMcpServers\": [\"company-github\", \"company-jira\"],\n \"deniedMcpServers\": [\"untrusted-local\"],\n \"allowedPlugins\": [\"company-review\"],\n \"deniedPlugins\": [\"untrusted-plugin\"],\n \"allowedPluginSources\": [\"company-marketplace\"],\n \"blockedMarketplaces\": [\"unknown-public-source\"],\n \"requireSignedPlugins\": true,\n \"trustedPluginKeySha256\": [\"\"],\n \"requirePluginCapabilityDeclarations\": true,\n \"requirePluginCapabilityConsent\": true,\n \"disableBypassPermissionsMode\": \"disable\",\n \"model\": \"managed-model\",\n \"env\": { \"AUDIT_ENABLED\": \"1\" }\n}\n```\n\n`allowManagedPermissionRulesOnly` 会忽略所有非 managed 的 allow/ask/deny,并禁用 REPL 的 always-allow 持久化;`allowManagedHooksOnly` 禁止 user/project/local/`--settings` hooks,但 managed hooks 在 `--safe-mode` 下仍执行;MCP allow/deny 在连接和启动 stdio server 之前生效,覆盖临时配置、注册配置、项目 `.mcp.json` 和 IDE/PDH bridge。`disableBypassPermissionsMode` 会拒绝 `--permission-mode bypassPermissions`。managed 文件存在但无法读取或 JSON 损坏时 Agent 失败关闭。`cc permissions list --json` 会返回 `managed` 与 `managedFile`,文本模式也会标出强制策略。\n\n插件安装策略在写数据库和安装 skills 之前执行。`requireSignedPlugins` 要求提供 manifest、二进制 Ed25519 detached signature 和 PEM 公钥:\n\n```bash\ncc plugin install company-review \\\n --source company-marketplace \\\n --manifest .claude-plugin/plugin.json \\\n --sha256 \\\n --signature plugin.sig \\\n --public-key company-plugin-key.pem\n```\n\n`deniedPlugins` 优先于 allowlist;source/marketplace 策略要求安装方显式声明 `--source`。启用强制签名时还必须配置受信公钥的 SPKI DER SHA-256 指纹,不能用任意自签公钥绕过。manifest SHA-256、签名或公钥信任不匹配,以及 manifest JSON 损坏都会失败关闭。`requirePluginCapabilityDeclarations` 会拒绝没有显式 `permissions` block 的 legacy 插件;`requirePluginCapabilityConsent` 除要求当前 capability 集已授权外,也隐含声明强制,不能通过删除 `permissions` 绕过 consent。单机可分别使用 `CC_REQUIRE_PLUGIN_CAPABILITIES=1` 和 `CC_REQUIRE_PLUGIN_CONSENT=1` 收紧。插件直接 URL MCP transport 会在连接前校验目标 hostname 是否落在声明的 `network` domains 中;stdio 子进程的网络/文件边界仍由 Process Broker 平台沙箱负责。\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n**`/permissions denials`**(Claude-Code 2.1.193 \"recent denials\" 平价):回看本会话被**拦下**的工具调用——shell-policy / ApprovalGate 档位 / settings 规则 / hook 拦截。每条显示工具+尝试的参数摘要、命中的 `via`(+规则名)、相对时间、原因文本(最近在上,封顶 20 条,纯内存)。**只记策略拒绝,不记普通执行失败**(命令退非零 / 文件不存在等本就有 `error` 但无拦截标记)。主 `/permissions` 视图在有拦截时附一行提示导向它。连续重复的同一拦截会**折叠成 `×N`**(模型撞墙后常反复重试,避免挤掉其它不同的拦截)。\n\n**Headless 拦截汇总**:非交互运行(`cc agent -p`)在结束时把被拦的工具调用汇总输出,三种 `--output-format` 都覆盖——text 打到 **stderr**(`N tool call(s) were denied by policy this run:` + 同款列表);stream-json 在最终 `result` 事件前发一条 `denials_summary` 事件(`{count, denials[]}`);**json** 在 result envelope 里带一个 `denials` 数组(仅有拦截时出现 → 无拦截时 envelope 逐字节不变)。让 CI / 脚本能一眼/可解析地看出 agent 被什么策略挡了(等价 REPL 的 `/permissions denials`)。**模型侧**:每条策略拒绝回给模型的 tool result 也是**可操作的**——明说这是策略决定、重试无用、该让用户介入(减少撞墙重试空耗 turn/token)。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n### `autoMode.classifyAllShell`(Claude-Code 2.1.193 平价)\n\nshell-policy 默认给一组**验证类命令**(`npm test` / `npm run lint` / `npm run build` /\n单文件 Playwright / `rg`)走**快车道 ALLOW**(风险 LOW,自动放行不弹确认)。在\n`.claude/settings.json` 里设 `{ \"autoMode\": { \"classifyAllShell\": true } }` 后,这组内置\n白名单也被**降级为 WARN**(风险 MEDIUM)→ 经 ApprovalGate 确认,**任何 shell 命令都不再静默自跑**。\n\n- 只影响**内置便利白名单**;硬黑名单(`rm`/`curl`/destructive git/disk)仍 **DENY**,未分类命令本就是 WARN。\n- 用户显式的 settings `allow` 规则(`Bash(npm run test:*)`)是**另一层有意预授权**,不受此开关影响。\n- 层级与其它设置一致(`~/.claude` < 项目 < `.local` < `--settings`,最近层胜);未设 = 关。\n- 无 ApprovalGate 时(纯 headless 无 gate)降级为 WARN 仍是 allowed——开关的\"牙齿\"来自交互档位的确认。\n\n## Agent Shell OS Sandbox\n\n`cc agent --sandbox` 会把 `run_shell` 放入一次性 Docker 容器执行,而不只是检查命令字符串:\n\n```bash\ncc agent --sandbox -p \"运行测试并修复失败\"\ncc agent --sandbox node:22-alpine -p \"检查构建\"\ncc agent --sandbox --sandbox-network -p \"更新依赖并运行测试\"\n```\n\n默认挂载当前工作区为可写 `/workspace`、禁用容器网络,只传 Agent 会话标识环境变量,不传宿主 API key。Docker 缺失或容器启动失败时会失败关闭。首版不支持 sandbox 内后台 shell。\n\n也可通过分层 `.claude/settings.json` 启用并配置沙箱。数组策略跨 user/project/local/managed 层累加,标量由更高优先级覆盖:\n\n```json\n{\n \"sandbox\": {\n \"enabled\": true,\n \"engine\": \"bubblewrap\",\n \"failIfUnavailable\": true,\n \"allowUnsandboxedCommands\": false,\n \"filesystem\": {\n \"allowWrite\": [\"/tmp/build\"],\n \"denyRead\": [\"~/.ssh\", \".env\"]\n },\n \"network\": {\n \"allowedDomains\": [\"registry.npmjs.org\"]\n },\n \"excludedCommands\": [\"docker\"]\n }\n}\n```\n\n`bubblewrap` 后端在 Linux/WSL2 使用只读宿主根、可写工作区、独立 PID/IPC/UTS/user namespace,默认不共享网络。Docker 后端目前只保证工作区挂载与全局断网;若配置细粒度文件策略会失败关闭并提示改用 `bubblewrap`。域名级网络策略需要独立代理,代理尚未配置时也会拒绝开启不受限网络,避免产生“策略已生效”的假象。\n\n## Credential 读取保护(Claude-Code 2.1.189 `sandbox.credentials` 平价)\n\ncc 不做 OS 沙箱,但同样的意图在**工具层**落地:Agent 不应把用户的密钥静默吸进模型上下文\n(可能被回显、写进转录、或被后续工具外泄)。两条读取路径被**确认优先 / headless fail-closed**\n拦截(与 sensitive-file **写**保护同款):\n\n- **`read_file`** 指向凭据文件:`.env`(及 `.env.local` / `.env.production` 等真实变体——但\n `.env.example` / `.env.sample` / `.env.template` 等模板**不拦**)、`~/.aws/credentials`、\n `~/.kube/config`、`~/.docker/config.json`、`~/.config/gh/hosts.yml`、`.npmrc` / `.netrc` /\n `.git-credentials` / `.pgpass`、私钥与证书材料(`*.pem` / `*.key` / `*.pfx` / `*.p12` /\n `*.jks` / `id_rsa` 等;`*.pub` / `*.crt` 公钥**不拦**)、`secrets.{json,yaml}`。\n- **`run_shell`** 读凭据:用 `cat` / `type` / `Get-Content` / `head` … 读上述凭据文件;\n `echo $ANTHROPIC_API_KEY` / `printf \"%s\" \"$AWS_SECRET_ACCESS_KEY\"` / `echo %GITHUB_TOKEN%` /\n `Write-Host $env:DB_PASSWORD` / `printenv OPENAI_API_KEY` 这类**打印密钥型环境变量**;\n 以及 `printenv` / `env` / `Get-ChildItem env:` 这类**全量 env dump**。复合命令的**每一段**\n 都查(`echo hi && cat .env` 也会被拦)。仅**消费**密钥而不回显的命令(`mytool --token $API_TOKEN`)\n 不拦——值不会落进输出。\n\n旁路与开关:\n\n- **显式 settings `allow` 规则**(`Read(./**)` / `Bash(cat:*)` 等)或交互确认 `ask` 已通过 →\n 预授权,不再拦。\n- 交互模式弹确认(一次按键);**headless 无确认器 → fail-closed 拒绝**,报\n `[Credential Guard] … requires confirmation — denied`。\n- `CC_CREDENTIAL_GUARD=0`(/ `false` / `no` / `off`)整体关闭。\n- **有意分歧**:`--safe-mode` **不**关闭本保护——和权限 deny 规则一样,这是**安全面**而非\n 自定义项,\"裸跑排查自定义\"绝不能放宽 Agent 能读到的密钥。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `Bash,PowerShell` 逗号 / `*` 通配 / `/regex/`(管道与逗号可混用,空段忽略)。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "8fcdd02873a566b507d86abcbbf7c9435f072da5": {"t": 1785754072.9250493, "n": 67640, "h": "13318216cde8457c", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n### 后台 Agent 会话\n\n长任务可以脱离当前终端运行:\n\n```bash\ncc agent --bg -p \"运行完整测试并修复失败\"\ncc agents background # 仅运行中\ncc agents background --all # 包含完成/失败/停止\ncc agents logs -n 200\ncc agents stop \n```\n\n后台 worker 与实际 Agent 进程独立,状态和日志保存在 `~/.chainlesschain/background-agents/`。状态文件不保存命令行参数,避免 `--api-key` 等凭据进入会话索引;启动 job 文件使用 `0600` 并由 worker 读取后立即删除。当前 `--bg` 不与 `--worktree` 同时使用,可以先进入目标 worktree 再启动后台 Agent。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | `CC_MAX_RETRIES`(原生)/ `CLAUDE_CODE_MAX_RETRIES`(平价),封顶 15 |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- **重试次数**:默认 2,可经 `CC_MAX_RETRIES` 或 `CLAUDE_CODE_MAX_RETRIES`(Claude-Code 2.1.186 平价)调节,**clamp 到 `[0, 15]`**(0=关重试;封顶防死端点变成多分钟指数退避挂起)。两值都设时 `CC_MAX_RETRIES` 胜;agent 与 chat 两路径同源。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Notebooks — `notebook_edit` + notebook-aware `read_file`(Claude-Code NotebookEdit/Read 平价)\n\nJupyter `.ipynb` 读写一对工具,补齐 cc agent 唯一缺的 Claude Code 工具。\n\n**读**:`read_file` 对 `.ipynb` 自动渲染成**紧凑单元清单**(每格 index + id + type + source,outputs 只摘要不 dump),而非原始 JSON——避免 base64 输出把上下文撑爆/被 50k 截断藏住单元。传 `raw:true` 取底层 JSON。\n\n```\nJupyter notebook — 2 cell(s), nbformat 4. Edit cells with the notebook_edit tool …\n\n── Cell 0 [code id=c1] ──\nx = 1\nprint(x)\n ⟨1 output(s) hidden⟩\n\n── Cell 1 [markdown id=m1] ──\n# Title\n```\n\n**写**:`notebook_edit` 改单个单元——\n\n| 参数 | 说明 |\n|---|---|\n| `path` | `.ipynb` 路径(必填) |\n| `edit_mode` | `replace`(默认)/ `insert` / `delete` |\n| `cell_id` / `cell_index` | 目标定位(cell_id 优先;从渲染清单里读 id) |\n| `cell_type` | `code` / `markdown`——`insert` 必填 |\n| `new_source` | 新单元源文本(replace/insert 必填) |\n\n- **replace**:覆盖目标格 source;若是 code 格,顺带清空 `outputs` + `execution_count`(旧输出会误导)。\n- **insert**:在目标格**之后**插新格(无目标→插到**顶部**),自动生成 cell id。\n- **delete**:移除目标格。\n- 源以保留行尾 `\\n` 的行数组存盘,1-space 缩进 + 尾换行重序列化(nbformat 习惯)。\n\n**典型流程**:`read_file notebook.ipynb`(看到各格 id)→ `notebook_edit {path, cell_id, new_source}`。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task`/`Agent` | `spawn_sub_agent` | 子 agent 类型 | `Agent(explorer)`、`Agent(executor,design)` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- **命名参数**(Claude-Code 2.1.178):`Tool(param:value)` 匹配任一具名实参,\n 如 `Bash(command:rm*)`、`mcp__db__query(table:users)`。\n- **子 agent 类型**(Claude-Code 2.1.186):`Agent(type)` / `Agent(typeA,typeB)`\n 按 `spawn_sub_agent` 调用的类型(`profile` / 具名 `agent` / `role`,取其一)匹配——\n 逗号列表任一命中即可、类型名大小写不敏感、支持 `*`/`?` glob。从 Claude-Code\n `settings.json` 直接移植的 `Agent(…)` 规则因此会被执行而非静默忽略。\n (`Task(profile:explorer)` 命名参数写法仍可用。)\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n### Managed Settings(组织强制策略)\n\n管理员可以部署不可被 user/project/local、`--settings` 或环境权限规则覆盖的策略文件:\n\n- Windows:`%ProgramData%\\ChainlessChain\\managed-settings.json`\n- Linux/macOS:`/etc/chainlesschain/managed-settings.json`\n- 测试或定制部署:`CC_MANAGED_SETTINGS=/absolute/path.json`\n\n```json\n{\n \"permissions\": {\n \"deny\": [\"Bash(curl:*)\", \"Read(./.env*)\"]\n },\n \"allowManagedPermissionRulesOnly\": true,\n \"allowManagedHooksOnly\": true,\n \"allowManagedMcpServersOnly\": true,\n \"allowedMcpServers\": [\"company-github\", \"company-jira\"],\n \"deniedMcpServers\": [\"untrusted-local\"],\n \"allowedPlugins\": [\"company-review\"],\n \"deniedPlugins\": [\"untrusted-plugin\"],\n \"allowedPluginSources\": [\"company-marketplace\"],\n \"blockedMarketplaces\": [\"unknown-public-source\"],\n \"requireSignedPlugins\": true,\n \"trustedPluginKeySha256\": [\"\"],\n \"disableBypassPermissionsMode\": \"disable\",\n \"model\": \"managed-model\",\n \"env\": { \"AUDIT_ENABLED\": \"1\" }\n}\n```\n\n`allowManagedPermissionRulesOnly` 会忽略所有非 managed 的 allow/ask/deny,并禁用 REPL 的 always-allow 持久化;`allowManagedHooksOnly` 禁止 user/project/local/`--settings` hooks,但 managed hooks 在 `--safe-mode` 下仍执行;MCP allow/deny 在连接和启动 stdio server 之前生效,覆盖临时配置、注册配置、项目 `.mcp.json` 和 IDE/PDH bridge。`disableBypassPermissionsMode` 会拒绝 `--permission-mode bypassPermissions`。managed 文件存在但无法读取或 JSON 损坏时 Agent 失败关闭。`cc permissions list --json` 会返回 `managed` 与 `managedFile`,文本模式也会标出强制策略。\n\n插件安装策略在写数据库和安装 skills 之前执行。`requireSignedPlugins` 要求提供 manifest、二进制 Ed25519 detached signature 和 PEM 公钥:\n\n```bash\ncc plugin install company-review \\\n --source company-marketplace \\\n --manifest .claude-plugin/plugin.json \\\n --sha256 \\\n --signature plugin.sig \\\n --public-key company-plugin-key.pem\n```\n\n`deniedPlugins` 优先于 allowlist;source/marketplace 策略要求安装方显式声明 `--source`。启用强制签名时还必须配置受信公钥的 SPKI DER SHA-256 指纹,不能用任意自签公钥绕过。manifest SHA-256、签名或公钥信任不匹配,以及 manifest JSON 损坏都会失败关闭。\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n**`/permissions denials`**(Claude-Code 2.1.193 \"recent denials\" 平价):回看本会话被**拦下**的工具调用——shell-policy / ApprovalGate 档位 / settings 规则 / hook 拦截。每条显示工具+尝试的参数摘要、命中的 `via`(+规则名)、相对时间、原因文本(最近在上,封顶 20 条,纯内存)。**只记策略拒绝,不记普通执行失败**(命令退非零 / 文件不存在等本就有 `error` 但无拦截标记)。主 `/permissions` 视图在有拦截时附一行提示导向它。连续重复的同一拦截会**折叠成 `×N`**(模型撞墙后常反复重试,避免挤掉其它不同的拦截)。\n\n**Headless 拦截汇总**:非交互运行(`cc agent -p`)在结束时把被拦的工具调用汇总输出,三种 `--output-format` 都覆盖——text 打到 **stderr**(`N tool call(s) were denied by policy this run:` + 同款列表);stream-json 在最终 `result` 事件前发一条 `denials_summary` 事件(`{count, denials[]}`);**json** 在 result envelope 里带一个 `denials` 数组(仅有拦截时出现 → 无拦截时 envelope 逐字节不变)。让 CI / 脚本能一眼/可解析地看出 agent 被什么策略挡了(等价 REPL 的 `/permissions denials`)。**模型侧**:每条策略拒绝回给模型的 tool result 也是**可操作的**——明说这是策略决定、重试无用、该让用户介入(减少撞墙重试空耗 turn/token)。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n### `autoMode.classifyAllShell`(Claude-Code 2.1.193 平价)\n\nshell-policy 默认给一组**验证类命令**(`npm test` / `npm run lint` / `npm run build` /\n单文件 Playwright / `rg`)走**快车道 ALLOW**(风险 LOW,自动放行不弹确认)。在\n`.claude/settings.json` 里设 `{ \"autoMode\": { \"classifyAllShell\": true } }` 后,这组内置\n白名单也被**降级为 WARN**(风险 MEDIUM)→ 经 ApprovalGate 确认,**任何 shell 命令都不再静默自跑**。\n\n- 只影响**内置便利白名单**;硬黑名单(`rm`/`curl`/destructive git/disk)仍 **DENY**,未分类命令本就是 WARN。\n- 用户显式的 settings `allow` 规则(`Bash(npm run test:*)`)是**另一层有意预授权**,不受此开关影响。\n- 层级与其它设置一致(`~/.claude` < 项目 < `.local` < `--settings`,最近层胜);未设 = 关。\n- 无 ApprovalGate 时(纯 headless 无 gate)降级为 WARN 仍是 allowed——开关的\"牙齿\"来自交互档位的确认。\n\n## Agent Shell OS Sandbox\n\n`cc agent --sandbox` 会把 `run_shell` 放入一次性 Docker 容器执行,而不只是检查命令字符串:\n\n```bash\ncc agent --sandbox -p \"运行测试并修复失败\"\ncc agent --sandbox node:22-alpine -p \"检查构建\"\ncc agent --sandbox --sandbox-network -p \"更新依赖并运行测试\"\n```\n\n默认挂载当前工作区为可写 `/workspace`、禁用容器网络,只传 Agent 会话标识环境变量,不传宿主 API key。Docker 缺失或容器启动失败时会失败关闭。首版不支持 sandbox 内后台 shell。\n\n也可通过分层 `.claude/settings.json` 启用并配置沙箱。数组策略跨 user/project/local/managed 层累加,标量由更高优先级覆盖:\n\n```json\n{\n \"sandbox\": {\n \"enabled\": true,\n \"engine\": \"bubblewrap\",\n \"failIfUnavailable\": true,\n \"allowUnsandboxedCommands\": false,\n \"filesystem\": {\n \"allowWrite\": [\"/tmp/build\"],\n \"denyRead\": [\"~/.ssh\", \".env\"]\n },\n \"network\": {\n \"allowedDomains\": [\"registry.npmjs.org\"]\n },\n \"excludedCommands\": [\"docker\"]\n }\n}\n```\n\n`bubblewrap` 后端在 Linux/WSL2 使用只读宿主根、可写工作区、独立 PID/IPC/UTS/user namespace,默认不共享网络。Docker 后端目前只保证工作区挂载与全局断网;若配置细粒度文件策略会失败关闭并提示改用 `bubblewrap`。域名级网络策略需要独立代理,代理尚未配置时也会拒绝开启不受限网络,避免产生“策略已生效”的假象。\n\n## Credential 读取保护(Claude-Code 2.1.189 `sandbox.credentials` 平价)\n\ncc 不做 OS 沙箱,但同样的意图在**工具层**落地:Agent 不应把用户的密钥静默吸进模型上下文\n(可能被回显、写进转录、或被后续工具外泄)。两条读取路径被**确认优先 / headless fail-closed**\n拦截(与 sensitive-file **写**保护同款):\n\n- **`read_file`** 指向凭据文件:`.env`(及 `.env.local` / `.env.production` 等真实变体——但\n `.env.example` / `.env.sample` / `.env.template` 等模板**不拦**)、`~/.aws/credentials`、\n `~/.kube/config`、`~/.docker/config.json`、`~/.config/gh/hosts.yml`、`.npmrc` / `.netrc` /\n `.git-credentials` / `.pgpass`、私钥与证书材料(`*.pem` / `*.key` / `*.pfx` / `*.p12` /\n `*.jks` / `id_rsa` 等;`*.pub` / `*.crt` 公钥**不拦**)、`secrets.{json,yaml}`。\n- **`run_shell`** 读凭据:用 `cat` / `type` / `Get-Content` / `head` … 读上述凭据文件;\n `echo $ANTHROPIC_API_KEY` / `printf \"%s\" \"$AWS_SECRET_ACCESS_KEY\"` / `echo %GITHUB_TOKEN%` /\n `Write-Host $env:DB_PASSWORD` / `printenv OPENAI_API_KEY` 这类**打印密钥型环境变量**;\n 以及 `printenv` / `env` / `Get-ChildItem env:` 这类**全量 env dump**。复合命令的**每一段**\n 都查(`echo hi && cat .env` 也会被拦)。仅**消费**密钥而不回显的命令(`mytool --token $API_TOKEN`)\n 不拦——值不会落进输出。\n\n旁路与开关:\n\n- **显式 settings `allow` 规则**(`Read(./**)` / `Bash(cat:*)` 等)或交互确认 `ask` 已通过 →\n 预授权,不再拦。\n- 交互模式弹确认(一次按键);**headless 无确认器 → fail-closed 拒绝**,报\n `[Credential Guard] … requires confirmation — denied`。\n- `CC_CREDENTIAL_GUARD=0`(/ `false` / `no` / `off`)整体关闭。\n- **有意分歧**:`--safe-mode` **不**关闭本保护——和权限 deny 规则一样,这是**安全面**而非\n 自定义项,\"裸跑排查自定义\"绝不能放宽 Agent 能读到的密钥。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `Bash,PowerShell` 逗号 / `*` 通配 / `/regex/`(管道与逗号可混用,空段忽略)。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "61ad1f4e0227d72726ec06cc6272d55deb6420be": {"t": 1785754074.5758755, "n": 67001, "h": "682064c2ce1fcff9", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n### 后台 Agent 会话\n\n长任务可以脱离当前终端运行:\n\n```bash\ncc agent --bg -p \"运行完整测试并修复失败\"\ncc agents background # 仅运行中\ncc agents background --all # 包含完成/失败/停止\ncc agents logs -n 200\ncc agents stop \n```\n\n后台 worker 与实际 Agent 进程独立,状态和日志保存在 `~/.chainlesschain/background-agents/`。状态文件不保存命令行参数,避免 `--api-key` 等凭据进入会话索引;启动 job 文件使用 `0600` 并由 worker 读取后立即删除。当前 `--bg` 不与 `--worktree` 同时使用,可以先进入目标 worktree 再启动后台 Agent。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | `CC_MAX_RETRIES`(原生)/ `CLAUDE_CODE_MAX_RETRIES`(平价),封顶 15 |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- **重试次数**:默认 2,可经 `CC_MAX_RETRIES` 或 `CLAUDE_CODE_MAX_RETRIES`(Claude-Code 2.1.186 平价)调节,**clamp 到 `[0, 15]`**(0=关重试;封顶防死端点变成多分钟指数退避挂起)。两值都设时 `CC_MAX_RETRIES` 胜;agent 与 chat 两路径同源。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Notebooks — `notebook_edit` + notebook-aware `read_file`(Claude-Code NotebookEdit/Read 平价)\n\nJupyter `.ipynb` 读写一对工具,补齐 cc agent 唯一缺的 Claude Code 工具。\n\n**读**:`read_file` 对 `.ipynb` 自动渲染成**紧凑单元清单**(每格 index + id + type + source,outputs 只摘要不 dump),而非原始 JSON——避免 base64 输出把上下文撑爆/被 50k 截断藏住单元。传 `raw:true` 取底层 JSON。\n\n```\nJupyter notebook — 2 cell(s), nbformat 4. Edit cells with the notebook_edit tool …\n\n── Cell 0 [code id=c1] ──\nx = 1\nprint(x)\n ⟨1 output(s) hidden⟩\n\n── Cell 1 [markdown id=m1] ──\n# Title\n```\n\n**写**:`notebook_edit` 改单个单元——\n\n| 参数 | 说明 |\n|---|---|\n| `path` | `.ipynb` 路径(必填) |\n| `edit_mode` | `replace`(默认)/ `insert` / `delete` |\n| `cell_id` / `cell_index` | 目标定位(cell_id 优先;从渲染清单里读 id) |\n| `cell_type` | `code` / `markdown`——`insert` 必填 |\n| `new_source` | 新单元源文本(replace/insert 必填) |\n\n- **replace**:覆盖目标格 source;若是 code 格,顺带清空 `outputs` + `execution_count`(旧输出会误导)。\n- **insert**:在目标格**之后**插新格(无目标→插到**顶部**),自动生成 cell id。\n- **delete**:移除目标格。\n- 源以保留行尾 `\\n` 的行数组存盘,1-space 缩进 + 尾换行重序列化(nbformat 习惯)。\n\n**典型流程**:`read_file notebook.ipynb`(看到各格 id)→ `notebook_edit {path, cell_id, new_source}`。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task`/`Agent` | `spawn_sub_agent` | 子 agent 类型 | `Agent(explorer)`、`Agent(executor,design)` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- **命名参数**(Claude-Code 2.1.178):`Tool(param:value)` 匹配任一具名实参,\n 如 `Bash(command:rm*)`、`mcp__db__query(table:users)`。\n- **子 agent 类型**(Claude-Code 2.1.186):`Agent(type)` / `Agent(typeA,typeB)`\n 按 `spawn_sub_agent` 调用的类型(`profile` / 具名 `agent` / `role`,取其一)匹配——\n 逗号列表任一命中即可、类型名大小写不敏感、支持 `*`/`?` glob。从 Claude-Code\n `settings.json` 直接移植的 `Agent(…)` 规则因此会被执行而非静默忽略。\n (`Task(profile:explorer)` 命名参数写法仍可用。)\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n### Managed Settings(组织强制策略)\n\n管理员可以部署不可被 user/project/local、`--settings` 或环境权限规则覆盖的策略文件:\n\n- Windows:`%ProgramData%\\ChainlessChain\\managed-settings.json`\n- Linux/macOS:`/etc/chainlesschain/managed-settings.json`\n- 测试或定制部署:`CC_MANAGED_SETTINGS=/absolute/path.json`\n\n```json\n{\n \"permissions\": {\n \"deny\": [\"Bash(curl:*)\", \"Read(./.env*)\"]\n },\n \"allowManagedPermissionRulesOnly\": true,\n \"allowManagedHooksOnly\": true,\n \"allowManagedMcpServersOnly\": true,\n \"allowedMcpServers\": [\"company-github\", \"company-jira\"],\n \"deniedMcpServers\": [\"untrusted-local\"],\n \"allowedPlugins\": [\"company-review\"],\n \"deniedPlugins\": [\"untrusted-plugin\"],\n \"allowedPluginSources\": [\"company-marketplace\"],\n \"blockedMarketplaces\": [\"unknown-public-source\"],\n \"requireSignedPlugins\": true,\n \"trustedPluginKeySha256\": [\"\"],\n \"disableBypassPermissionsMode\": \"disable\",\n \"model\": \"managed-model\",\n \"env\": { \"AUDIT_ENABLED\": \"1\" }\n}\n```\n\n`allowManagedPermissionRulesOnly` 会忽略所有非 managed 的 allow/ask/deny,并禁用 REPL 的 always-allow 持久化;`allowManagedHooksOnly` 禁止 user/project/local/`--settings` hooks,但 managed hooks 在 `--safe-mode` 下仍执行;MCP allow/deny 在连接和启动 stdio server 之前生效,覆盖临时配置、注册配置、项目 `.mcp.json` 和 IDE/PDH bridge。`disableBypassPermissionsMode` 会拒绝 `--permission-mode bypassPermissions`。managed 文件存在但无法读取或 JSON 损坏时 Agent 失败关闭。`cc permissions list --json` 会返回 `managed` 与 `managedFile`,文本模式也会标出强制策略。\n\n插件安装策略在写数据库和安装 skills 之前执行。`requireSignedPlugins` 要求提供 manifest、二进制 Ed25519 detached signature 和 PEM 公钥:\n\n```bash\ncc plugin install company-review \\\n --source company-marketplace \\\n --manifest .claude-plugin/plugin.json \\\n --sha256 \\\n --signature plugin.sig \\\n --public-key company-plugin-key.pem\n```\n\n`deniedPlugins` 优先于 allowlist;source/marketplace 策略要求安装方显式声明 `--source`。启用强制签名时还必须配置受信公钥的 SPKI DER SHA-256 指纹,不能用任意自签公钥绕过。manifest SHA-256、签名或公钥信任不匹配,以及 manifest JSON 损坏都会失败关闭。\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n**`/permissions denials`**(Claude-Code 2.1.193 \"recent denials\" 平价):回看本会话被**拦下**的工具调用——shell-policy / ApprovalGate 档位 / settings 规则 / hook 拦截。每条显示工具+尝试的参数摘要、命中的 `via`(+规则名)、相对时间、原因文本(最近在上,封顶 20 条,纯内存)。**只记策略拒绝,不记普通执行失败**(命令退非零 / 文件不存在等本就有 `error` 但无拦截标记)。主 `/permissions` 视图在有拦截时附一行提示导向它。连续重复的同一拦截会**折叠成 `×N`**(模型撞墙后常反复重试,避免挤掉其它不同的拦截)。\n\n**Headless 拦截汇总**:非交互运行(`cc agent -p`)在结束时把被拦的工具调用汇总输出,三种 `--output-format` 都覆盖——text 打到 **stderr**(`N tool call(s) were denied by policy this run:` + 同款列表);stream-json 在最终 `result` 事件前发一条 `denials_summary` 事件(`{count, denials[]}`);**json** 在 result envelope 里带一个 `denials` 数组(仅有拦截时出现 → 无拦截时 envelope 逐字节不变)。让 CI / 脚本能一眼/可解析地看出 agent 被什么策略挡了(等价 REPL 的 `/permissions denials`)。**模型侧**:每条策略拒绝回给模型的 tool result 也是**可操作的**——明说这是策略决定、重试无用、该让用户介入(减少撞墙重试空耗 turn/token)。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n### `autoMode.classifyAllShell`(Claude-Code 2.1.193 平价)\n\nshell-policy 默认给一组**验证类命令**(`npm test` / `npm run lint` / `npm run build` /\n单文件 Playwright / `rg`)走**快车道 ALLOW**(风险 LOW,自动放行不弹确认)。在\n`.claude/settings.json` 里设 `{ \"autoMode\": { \"classifyAllShell\": true } }` 后,这组内置\n白名单也被**降级为 WARN**(风险 MEDIUM)→ 经 ApprovalGate 确认,**任何 shell 命令都不再静默自跑**。\n\n- 只影响**内置便利白名单**;硬黑名单(`rm`/`curl`/destructive git/disk)仍 **DENY**,未分类命令本就是 WARN。\n- 用户显式的 settings `allow` 规则(`Bash(npm run test:*)`)是**另一层有意预授权**,不受此开关影响。\n- 层级与其它设置一致(`~/.claude` < 项目 < `.local` < `--settings`,最近层胜);未设 = 关。\n- 无 ApprovalGate 时(纯 headless 无 gate)降级为 WARN 仍是 allowed——开关的\"牙齿\"来自交互档位的确认。\n\n## Agent Shell OS Sandbox\n\n`cc agent --sandbox` 会把 `run_shell` 放入一次性 Docker 容器执行,而不只是检查命令字符串:\n\n```bash\ncc agent --sandbox -p \"运行测试并修复失败\"\ncc agent --sandbox node:22-alpine -p \"检查构建\"\ncc agent --sandbox --sandbox-network -p \"更新依赖并运行测试\"\n```\n\n默认挂载当前工作区为可写 `/workspace`、禁用容器网络,只传 Agent 会话标识环境变量,不传宿主 API key。Docker 缺失或容器启动失败时会失败关闭。首版不支持 sandbox 内后台 shell。\n\n## Credential 读取保护(Claude-Code 2.1.189 `sandbox.credentials` 平价)\n\ncc 不做 OS 沙箱,但同样的意图在**工具层**落地:Agent 不应把用户的密钥静默吸进模型上下文\n(可能被回显、写进转录、或被后续工具外泄)。两条读取路径被**确认优先 / headless fail-closed**\n拦截(与 sensitive-file **写**保护同款):\n\n- **`read_file`** 指向凭据文件:`.env`(及 `.env.local` / `.env.production` 等真实变体——但\n `.env.example` / `.env.sample` / `.env.template` 等模板**不拦**)、`~/.aws/credentials`、\n `~/.kube/config`、`~/.docker/config.json`、`~/.config/gh/hosts.yml`、`.npmrc` / `.netrc` /\n `.git-credentials` / `.pgpass`、私钥与证书材料(`*.pem` / `*.key` / `*.pfx` / `*.p12` /\n `*.jks` / `id_rsa` 等;`*.pub` / `*.crt` 公钥**不拦**)、`secrets.{json,yaml}`。\n- **`run_shell`** 读凭据:用 `cat` / `type` / `Get-Content` / `head` … 读上述凭据文件;\n `echo $ANTHROPIC_API_KEY` / `printf \"%s\" \"$AWS_SECRET_ACCESS_KEY\"` / `echo %GITHUB_TOKEN%` /\n `Write-Host $env:DB_PASSWORD` / `printenv OPENAI_API_KEY` 这类**打印密钥型环境变量**;\n 以及 `printenv` / `env` / `Get-ChildItem env:` 这类**全量 env dump**。复合命令的**每一段**\n 都查(`echo hi && cat .env` 也会被拦)。仅**消费**密钥而不回显的命令(`mytool --token $API_TOKEN`)\n 不拦——值不会落进输出。\n\n旁路与开关:\n\n- **显式 settings `allow` 规则**(`Read(./**)` / `Bash(cat:*)` 等)或交互确认 `ask` 已通过 →\n 预授权,不再拦。\n- 交互模式弹确认(一次按键);**headless 无确认器 → fail-closed 拒绝**,报\n `[Credential Guard] … requires confirmation — denied`。\n- `CC_CREDENTIAL_GUARD=0`(/ `false` / `no` / `off`)整体关闭。\n- **有意分歧**:`--safe-mode` **不**关闭本保护——和权限 deny 规则一样,这是**安全面**而非\n 自定义项,\"裸跑排查自定义\"绝不能放宽 Agent 能读到的密钥。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `Bash,PowerShell` 逗号 / `*` 通配 / `/regex/`(管道与逗号可混用,空段忽略)。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "ce50c0eed992fe2f868474e9dab3e32cab140a81": {"t": 1785754076.28547, "n": 64323, "h": "cad2b6c03dd2c0fe", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | `CC_MAX_RETRIES`(原生)/ `CLAUDE_CODE_MAX_RETRIES`(平价),封顶 15 |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- **重试次数**:默认 2,可经 `CC_MAX_RETRIES` 或 `CLAUDE_CODE_MAX_RETRIES`(Claude-Code 2.1.186 平价)调节,**clamp 到 `[0, 15]`**(0=关重试;封顶防死端点变成多分钟指数退避挂起)。两值都设时 `CC_MAX_RETRIES` 胜;agent 与 chat 两路径同源。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Notebooks — `notebook_edit` + notebook-aware `read_file`(Claude-Code NotebookEdit/Read 平价)\n\nJupyter `.ipynb` 读写一对工具,补齐 cc agent 唯一缺的 Claude Code 工具。\n\n**读**:`read_file` 对 `.ipynb` 自动渲染成**紧凑单元清单**(每格 index + id + type + source,outputs 只摘要不 dump),而非原始 JSON——避免 base64 输出把上下文撑爆/被 50k 截断藏住单元。传 `raw:true` 取底层 JSON。\n\n```\nJupyter notebook — 2 cell(s), nbformat 4. Edit cells with the notebook_edit tool …\n\n── Cell 0 [code id=c1] ──\nx = 1\nprint(x)\n ⟨1 output(s) hidden⟩\n\n── Cell 1 [markdown id=m1] ──\n# Title\n```\n\n**写**:`notebook_edit` 改单个单元——\n\n| 参数 | 说明 |\n|---|---|\n| `path` | `.ipynb` 路径(必填) |\n| `edit_mode` | `replace`(默认)/ `insert` / `delete` |\n| `cell_id` / `cell_index` | 目标定位(cell_id 优先;从渲染清单里读 id) |\n| `cell_type` | `code` / `markdown`——`insert` 必填 |\n| `new_source` | 新单元源文本(replace/insert 必填) |\n\n- **replace**:覆盖目标格 source;若是 code 格,顺带清空 `outputs` + `execution_count`(旧输出会误导)。\n- **insert**:在目标格**之后**插新格(无目标→插到**顶部**),自动生成 cell id。\n- **delete**:移除目标格。\n- 源以保留行尾 `\\n` 的行数组存盘,1-space 缩进 + 尾换行重序列化(nbformat 习惯)。\n\n**典型流程**:`read_file notebook.ipynb`(看到各格 id)→ `notebook_edit {path, cell_id, new_source}`。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task`/`Agent` | `spawn_sub_agent` | 子 agent 类型 | `Agent(explorer)`、`Agent(executor,design)` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- **命名参数**(Claude-Code 2.1.178):`Tool(param:value)` 匹配任一具名实参,\n 如 `Bash(command:rm*)`、`mcp__db__query(table:users)`。\n- **子 agent 类型**(Claude-Code 2.1.186):`Agent(type)` / `Agent(typeA,typeB)`\n 按 `spawn_sub_agent` 调用的类型(`profile` / 具名 `agent` / `role`,取其一)匹配——\n 逗号列表任一命中即可、类型名大小写不敏感、支持 `*`/`?` glob。从 Claude-Code\n `settings.json` 直接移植的 `Agent(…)` 规则因此会被执行而非静默忽略。\n (`Task(profile:explorer)` 命名参数写法仍可用。)\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n**`/permissions denials`**(Claude-Code 2.1.193 \"recent denials\" 平价):回看本会话被**拦下**的工具调用——shell-policy / ApprovalGate 档位 / settings 规则 / hook 拦截。每条显示工具+尝试的参数摘要、命中的 `via`(+规则名)、相对时间、原因文本(最近在上,封顶 20 条,纯内存)。**只记策略拒绝,不记普通执行失败**(命令退非零 / 文件不存在等本就有 `error` 但无拦截标记)。主 `/permissions` 视图在有拦截时附一行提示导向它。连续重复的同一拦截会**折叠成 `×N`**(模型撞墙后常反复重试,避免挤掉其它不同的拦截)。\n\n**Headless 拦截汇总**:非交互运行(`cc agent -p`)在结束时把被拦的工具调用汇总输出,三种 `--output-format` 都覆盖——text 打到 **stderr**(`N tool call(s) were denied by policy this run:` + 同款列表);stream-json 在最终 `result` 事件前发一条 `denials_summary` 事件(`{count, denials[]}`);**json** 在 result envelope 里带一个 `denials` 数组(仅有拦截时出现 → 无拦截时 envelope 逐字节不变)。让 CI / 脚本能一眼/可解析地看出 agent 被什么策略挡了(等价 REPL 的 `/permissions denials`)。**模型侧**:每条策略拒绝回给模型的 tool result 也是**可操作的**——明说这是策略决定、重试无用、该让用户介入(减少撞墙重试空耗 turn/token)。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n### `autoMode.classifyAllShell`(Claude-Code 2.1.193 平价)\n\nshell-policy 默认给一组**验证类命令**(`npm test` / `npm run lint` / `npm run build` /\n单文件 Playwright / `rg`)走**快车道 ALLOW**(风险 LOW,自动放行不弹确认)。在\n`.claude/settings.json` 里设 `{ \"autoMode\": { \"classifyAllShell\": true } }` 后,这组内置\n白名单也被**降级为 WARN**(风险 MEDIUM)→ 经 ApprovalGate 确认,**任何 shell 命令都不再静默自跑**。\n\n- 只影响**内置便利白名单**;硬黑名单(`rm`/`curl`/destructive git/disk)仍 **DENY**,未分类命令本就是 WARN。\n- 用户显式的 settings `allow` 规则(`Bash(npm run test:*)`)是**另一层有意预授权**,不受此开关影响。\n- 层级与其它设置一致(`~/.claude` < 项目 < `.local` < `--settings`,最近层胜);未设 = 关。\n- 无 ApprovalGate 时(纯 headless 无 gate)降级为 WARN 仍是 allowed——开关的\"牙齿\"来自交互档位的确认。\n\n## Credential 读取保护(Claude-Code 2.1.189 `sandbox.credentials` 平价)\n\ncc 不做 OS 沙箱,但同样的意图在**工具层**落地:Agent 不应把用户的密钥静默吸进模型上下文\n(可能被回显、写进转录、或被后续工具外泄)。两条读取路径被**确认优先 / headless fail-closed**\n拦截(与 sensitive-file **写**保护同款):\n\n- **`read_file`** 指向凭据文件:`.env`(及 `.env.local` / `.env.production` 等真实变体——但\n `.env.example` / `.env.sample` / `.env.template` 等模板**不拦**)、`~/.aws/credentials`、\n `~/.kube/config`、`~/.docker/config.json`、`~/.config/gh/hosts.yml`、`.npmrc` / `.netrc` /\n `.git-credentials` / `.pgpass`、私钥与证书材料(`*.pem` / `*.key` / `*.pfx` / `*.p12` /\n `*.jks` / `id_rsa` 等;`*.pub` / `*.crt` 公钥**不拦**)、`secrets.{json,yaml}`。\n- **`run_shell`** 读凭据:用 `cat` / `type` / `Get-Content` / `head` … 读上述凭据文件;\n `echo $ANTHROPIC_API_KEY` / `printf \"%s\" \"$AWS_SECRET_ACCESS_KEY\"` / `echo %GITHUB_TOKEN%` /\n `Write-Host $env:DB_PASSWORD` / `printenv OPENAI_API_KEY` 这类**打印密钥型环境变量**;\n 以及 `printenv` / `env` / `Get-ChildItem env:` 这类**全量 env dump**。复合命令的**每一段**\n 都查(`echo hi && cat .env` 也会被拦)。仅**消费**密钥而不回显的命令(`mytool --token $API_TOKEN`)\n 不拦——值不会落进输出。\n\n旁路与开关:\n\n- **显式 settings `allow` 规则**(`Read(./**)` / `Bash(cat:*)` 等)或交互确认 `ask` 已通过 →\n 预授权,不再拦。\n- 交互模式弹确认(一次按键);**headless 无确认器 → fail-closed 拒绝**,报\n `[Credential Guard] … requires confirmation — denied`。\n- `CC_CREDENTIAL_GUARD=0`(/ `false` / `no` / `off`)整体关闭。\n- **有意分歧**:`--safe-mode` **不**关闭本保护——和权限 deny 规则一样,这是**安全面**而非\n 自定义项,\"裸跑排查自定义\"绝不能放宽 Agent 能读到的密钥。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `Bash,PowerShell` 逗号 / `*` 通配 / `/regex/`(管道与逗号可混用,空段忽略)。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "9e7e2eb4414c50db9be7d9926e0446e6eb91f2f4": {"t": 1785754077.8947358, "n": 64221, "h": "9522c15beb38bb4a", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | `CC_MAX_RETRIES`(原生)/ `CLAUDE_CODE_MAX_RETRIES`(平价),封顶 15 |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- **重试次数**:默认 2,可经 `CC_MAX_RETRIES` 或 `CLAUDE_CODE_MAX_RETRIES`(Claude-Code 2.1.186 平价)调节,**clamp 到 `[0, 15]`**(0=关重试;封顶防死端点变成多分钟指数退避挂起)。两值都设时 `CC_MAX_RETRIES` 胜;agent 与 chat 两路径同源。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Notebooks — `notebook_edit` + notebook-aware `read_file`(Claude-Code NotebookEdit/Read 平价)\n\nJupyter `.ipynb` 读写一对工具,补齐 cc agent 唯一缺的 Claude Code 工具。\n\n**读**:`read_file` 对 `.ipynb` 自动渲染成**紧凑单元清单**(每格 index + id + type + source,outputs 只摘要不 dump),而非原始 JSON——避免 base64 输出把上下文撑爆/被 50k 截断藏住单元。传 `raw:true` 取底层 JSON。\n\n```\nJupyter notebook — 2 cell(s), nbformat 4. Edit cells with the notebook_edit tool …\n\n── Cell 0 [code id=c1] ──\nx = 1\nprint(x)\n ⟨1 output(s) hidden⟩\n\n── Cell 1 [markdown id=m1] ──\n# Title\n```\n\n**写**:`notebook_edit` 改单个单元——\n\n| 参数 | 说明 |\n|---|---|\n| `path` | `.ipynb` 路径(必填) |\n| `edit_mode` | `replace`(默认)/ `insert` / `delete` |\n| `cell_id` / `cell_index` | 目标定位(cell_id 优先;从渲染清单里读 id) |\n| `cell_type` | `code` / `markdown`——`insert` 必填 |\n| `new_source` | 新单元源文本(replace/insert 必填) |\n\n- **replace**:覆盖目标格 source;若是 code 格,顺带清空 `outputs` + `execution_count`(旧输出会误导)。\n- **insert**:在目标格**之后**插新格(无目标→插到**顶部**),自动生成 cell id。\n- **delete**:移除目标格。\n- 源以保留行尾 `\\n` 的行数组存盘,1-space 缩进 + 尾换行重序列化(nbformat 习惯)。\n\n**典型流程**:`read_file notebook.ipynb`(看到各格 id)→ `notebook_edit {path, cell_id, new_source}`。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task`/`Agent` | `spawn_sub_agent` | 子 agent 类型 | `Agent(explorer)`、`Agent(executor,design)` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- **命名参数**(Claude-Code 2.1.178):`Tool(param:value)` 匹配任一具名实参,\n 如 `Bash(command:rm*)`、`mcp__db__query(table:users)`。\n- **子 agent 类型**(Claude-Code 2.1.186):`Agent(type)` / `Agent(typeA,typeB)`\n 按 `spawn_sub_agent` 调用的类型(`profile` / 具名 `agent` / `role`,取其一)匹配——\n 逗号列表任一命中即可、类型名大小写不敏感、支持 `*`/`?` glob。从 Claude-Code\n `settings.json` 直接移植的 `Agent(…)` 规则因此会被执行而非静默忽略。\n (`Task(profile:explorer)` 命名参数写法仍可用。)\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n**`/permissions denials`**(Claude-Code 2.1.193 \"recent denials\" 平价):回看本会话被**拦下**的工具调用——shell-policy / ApprovalGate 档位 / settings 规则 / hook 拦截。每条显示工具+尝试的参数摘要、命中的 `via`(+规则名)、相对时间、原因文本(最近在上,封顶 20 条,纯内存)。**只记策略拒绝,不记普通执行失败**(命令退非零 / 文件不存在等本就有 `error` 但无拦截标记)。主 `/permissions` 视图在有拦截时附一行提示导向它。连续重复的同一拦截会**折叠成 `×N`**(模型撞墙后常反复重试,避免挤掉其它不同的拦截)。\n\n**Headless 拦截汇总**:非交互运行(`cc agent -p`)在结束时把被拦的工具调用汇总输出——text 模式打到 **stderr**(`N tool call(s) were denied by policy this run:` + 同款列表),stream-json 模式在最终 `result` 事件前发一条 `denials_summary` 事件(`{count, denials[]}`)。让 CI / 脚本能一眼看出 agent 被什么策略挡了(等价 REPL 的 `/permissions denials`)。**模型侧**:每条策略拒绝回给模型的 tool result 也是**可操作的**——明说这是策略决定、重试无用、该让用户介入(减少撞墙重试空耗 turn/token)。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n### `autoMode.classifyAllShell`(Claude-Code 2.1.193 平价)\n\nshell-policy 默认给一组**验证类命令**(`npm test` / `npm run lint` / `npm run build` /\n单文件 Playwright / `rg`)走**快车道 ALLOW**(风险 LOW,自动放行不弹确认)。在\n`.claude/settings.json` 里设 `{ \"autoMode\": { \"classifyAllShell\": true } }` 后,这组内置\n白名单也被**降级为 WARN**(风险 MEDIUM)→ 经 ApprovalGate 确认,**任何 shell 命令都不再静默自跑**。\n\n- 只影响**内置便利白名单**;硬黑名单(`rm`/`curl`/destructive git/disk)仍 **DENY**,未分类命令本就是 WARN。\n- 用户显式的 settings `allow` 规则(`Bash(npm run test:*)`)是**另一层有意预授权**,不受此开关影响。\n- 层级与其它设置一致(`~/.claude` < 项目 < `.local` < `--settings`,最近层胜);未设 = 关。\n- 无 ApprovalGate 时(纯 headless 无 gate)降级为 WARN 仍是 allowed——开关的\"牙齿\"来自交互档位的确认。\n\n## Credential 读取保护(Claude-Code 2.1.189 `sandbox.credentials` 平价)\n\ncc 不做 OS 沙箱,但同样的意图在**工具层**落地:Agent 不应把用户的密钥静默吸进模型上下文\n(可能被回显、写进转录、或被后续工具外泄)。两条读取路径被**确认优先 / headless fail-closed**\n拦截(与 sensitive-file **写**保护同款):\n\n- **`read_file`** 指向凭据文件:`.env`(及 `.env.local` / `.env.production` 等真实变体——但\n `.env.example` / `.env.sample` / `.env.template` 等模板**不拦**)、`~/.aws/credentials`、\n `~/.kube/config`、`~/.docker/config.json`、`~/.config/gh/hosts.yml`、`.npmrc` / `.netrc` /\n `.git-credentials` / `.pgpass`、私钥与证书材料(`*.pem` / `*.key` / `*.pfx` / `*.p12` /\n `*.jks` / `id_rsa` 等;`*.pub` / `*.crt` 公钥**不拦**)、`secrets.{json,yaml}`。\n- **`run_shell`** 读凭据:用 `cat` / `type` / `Get-Content` / `head` … 读上述凭据文件;\n `echo $ANTHROPIC_API_KEY` / `printf \"%s\" \"$AWS_SECRET_ACCESS_KEY\"` / `echo %GITHUB_TOKEN%` /\n `Write-Host $env:DB_PASSWORD` / `printenv OPENAI_API_KEY` 这类**打印密钥型环境变量**;\n 以及 `printenv` / `env` / `Get-ChildItem env:` 这类**全量 env dump**。复合命令的**每一段**\n 都查(`echo hi && cat .env` 也会被拦)。仅**消费**密钥而不回显的命令(`mytool --token $API_TOKEN`)\n 不拦——值不会落进输出。\n\n旁路与开关:\n\n- **显式 settings `allow` 规则**(`Read(./**)` / `Bash(cat:*)` 等)或交互确认 `ask` 已通过 →\n 预授权,不再拦。\n- 交互模式弹确认(一次按键);**headless 无确认器 → fail-closed 拒绝**,报\n `[Credential Guard] … requires confirmation — denied`。\n- `CC_CREDENTIAL_GUARD=0`(/ `false` / `no` / `off`)整体关闭。\n- **有意分歧**:`--safe-mode` **不**关闭本保护——和权限 deny 规则一样,这是**安全面**而非\n 自定义项,\"裸跑排查自定义\"绝不能放宽 Agent 能读到的密钥。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `Bash,PowerShell` 逗号 / `*` 通配 / `/regex/`(管道与逗号可混用,空段忽略)。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "03481af76f6dc4c3fa0787cff802f533f4d40ec2": {"t": 1785754079.501577, "n": 63812, "h": "94b84ac8ee8c486f", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | `CC_MAX_RETRIES`(原生)/ `CLAUDE_CODE_MAX_RETRIES`(平价),封顶 15 |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- **重试次数**:默认 2,可经 `CC_MAX_RETRIES` 或 `CLAUDE_CODE_MAX_RETRIES`(Claude-Code 2.1.186 平价)调节,**clamp 到 `[0, 15]`**(0=关重试;封顶防死端点变成多分钟指数退避挂起)。两值都设时 `CC_MAX_RETRIES` 胜;agent 与 chat 两路径同源。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Notebooks — `notebook_edit` + notebook-aware `read_file`(Claude-Code NotebookEdit/Read 平价)\n\nJupyter `.ipynb` 读写一对工具,补齐 cc agent 唯一缺的 Claude Code 工具。\n\n**读**:`read_file` 对 `.ipynb` 自动渲染成**紧凑单元清单**(每格 index + id + type + source,outputs 只摘要不 dump),而非原始 JSON——避免 base64 输出把上下文撑爆/被 50k 截断藏住单元。传 `raw:true` 取底层 JSON。\n\n```\nJupyter notebook — 2 cell(s), nbformat 4. Edit cells with the notebook_edit tool …\n\n── Cell 0 [code id=c1] ──\nx = 1\nprint(x)\n ⟨1 output(s) hidden⟩\n\n── Cell 1 [markdown id=m1] ──\n# Title\n```\n\n**写**:`notebook_edit` 改单个单元——\n\n| 参数 | 说明 |\n|---|---|\n| `path` | `.ipynb` 路径(必填) |\n| `edit_mode` | `replace`(默认)/ `insert` / `delete` |\n| `cell_id` / `cell_index` | 目标定位(cell_id 优先;从渲染清单里读 id) |\n| `cell_type` | `code` / `markdown`——`insert` 必填 |\n| `new_source` | 新单元源文本(replace/insert 必填) |\n\n- **replace**:覆盖目标格 source;若是 code 格,顺带清空 `outputs` + `execution_count`(旧输出会误导)。\n- **insert**:在目标格**之后**插新格(无目标→插到**顶部**),自动生成 cell id。\n- **delete**:移除目标格。\n- 源以保留行尾 `\\n` 的行数组存盘,1-space 缩进 + 尾换行重序列化(nbformat 习惯)。\n\n**典型流程**:`read_file notebook.ipynb`(看到各格 id)→ `notebook_edit {path, cell_id, new_source}`。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task`/`Agent` | `spawn_sub_agent` | 子 agent 类型 | `Agent(explorer)`、`Agent(executor,design)` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- **命名参数**(Claude-Code 2.1.178):`Tool(param:value)` 匹配任一具名实参,\n 如 `Bash(command:rm*)`、`mcp__db__query(table:users)`。\n- **子 agent 类型**(Claude-Code 2.1.186):`Agent(type)` / `Agent(typeA,typeB)`\n 按 `spawn_sub_agent` 调用的类型(`profile` / 具名 `agent` / `role`,取其一)匹配——\n 逗号列表任一命中即可、类型名大小写不敏感、支持 `*`/`?` glob。从 Claude-Code\n `settings.json` 直接移植的 `Agent(…)` 规则因此会被执行而非静默忽略。\n (`Task(profile:explorer)` 命名参数写法仍可用。)\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n**`/permissions denials`**(Claude-Code 2.1.193 \"recent denials\" 平价):回看本会话被**拦下**的工具调用——shell-policy / ApprovalGate 档位 / settings 规则 / hook 拦截。每条显示工具+尝试的参数摘要、命中的 `via`(+规则名)、相对时间、原因文本(最近在上,封顶 20 条,纯内存)。**只记策略拒绝,不记普通执行失败**(命令退非零 / 文件不存在等本就有 `error` 但无拦截标记)。主 `/permissions` 视图在有拦截时附一行提示导向它。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n### `autoMode.classifyAllShell`(Claude-Code 2.1.193 平价)\n\nshell-policy 默认给一组**验证类命令**(`npm test` / `npm run lint` / `npm run build` /\n单文件 Playwright / `rg`)走**快车道 ALLOW**(风险 LOW,自动放行不弹确认)。在\n`.claude/settings.json` 里设 `{ \"autoMode\": { \"classifyAllShell\": true } }` 后,这组内置\n白名单也被**降级为 WARN**(风险 MEDIUM)→ 经 ApprovalGate 确认,**任何 shell 命令都不再静默自跑**。\n\n- 只影响**内置便利白名单**;硬黑名单(`rm`/`curl`/destructive git/disk)仍 **DENY**,未分类命令本就是 WARN。\n- 用户显式的 settings `allow` 规则(`Bash(npm run test:*)`)是**另一层有意预授权**,不受此开关影响。\n- 层级与其它设置一致(`~/.claude` < 项目 < `.local` < `--settings`,最近层胜);未设 = 关。\n- 无 ApprovalGate 时(纯 headless 无 gate)降级为 WARN 仍是 allowed——开关的\"牙齿\"来自交互档位的确认。\n\n## Credential 读取保护(Claude-Code 2.1.189 `sandbox.credentials` 平价)\n\ncc 不做 OS 沙箱,但同样的意图在**工具层**落地:Agent 不应把用户的密钥静默吸进模型上下文\n(可能被回显、写进转录、或被后续工具外泄)。两条读取路径被**确认优先 / headless fail-closed**\n拦截(与 sensitive-file **写**保护同款):\n\n- **`read_file`** 指向凭据文件:`.env`(及 `.env.local` / `.env.production` 等真实变体——但\n `.env.example` / `.env.sample` / `.env.template` 等模板**不拦**)、`~/.aws/credentials`、\n `~/.kube/config`、`~/.docker/config.json`、`~/.config/gh/hosts.yml`、`.npmrc` / `.netrc` /\n `.git-credentials` / `.pgpass`、私钥与证书材料(`*.pem` / `*.key` / `*.pfx` / `*.p12` /\n `*.jks` / `id_rsa` 等;`*.pub` / `*.crt` 公钥**不拦**)、`secrets.{json,yaml}`。\n- **`run_shell`** 读凭据:用 `cat` / `type` / `Get-Content` / `head` … 读上述凭据文件;\n `echo $ANTHROPIC_API_KEY` / `printf \"%s\" \"$AWS_SECRET_ACCESS_KEY\"` / `echo %GITHUB_TOKEN%` /\n `Write-Host $env:DB_PASSWORD` / `printenv OPENAI_API_KEY` 这类**打印密钥型环境变量**;\n 以及 `printenv` / `env` / `Get-ChildItem env:` 这类**全量 env dump**。复合命令的**每一段**\n 都查(`echo hi && cat .env` 也会被拦)。仅**消费**密钥而不回显的命令(`mytool --token $API_TOKEN`)\n 不拦——值不会落进输出。\n\n旁路与开关:\n\n- **显式 settings `allow` 规则**(`Read(./**)` / `Bash(cat:*)` 等)或交互确认 `ask` 已通过 →\n 预授权,不再拦。\n- 交互模式弹确认(一次按键);**headless 无确认器 → fail-closed 拒绝**,报\n `[Credential Guard] … requires confirmation — denied`。\n- `CC_CREDENTIAL_GUARD=0`(/ `false` / `no` / `off`)整体关闭。\n- **有意分歧**:`--safe-mode` **不**关闭本保护——和权限 deny 规则一样,这是**安全面**而非\n 自定义项,\"裸跑排查自定义\"绝不能放宽 Agent 能读到的密钥。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `Bash,PowerShell` 逗号 / `*` 通配 / `/regex/`(管道与逗号可混用,空段忽略)。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "5d3202d7240082b9215aba65b4db136c8dc8bbbe": {"t": 1785754081.1732945, "n": 63522, "h": "63125772c0647907", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | `CC_MAX_RETRIES`(原生)/ `CLAUDE_CODE_MAX_RETRIES`(平价),封顶 15 |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- **重试次数**:默认 2,可经 `CC_MAX_RETRIES` 或 `CLAUDE_CODE_MAX_RETRIES`(Claude-Code 2.1.186 平价)调节,**clamp 到 `[0, 15]`**(0=关重试;封顶防死端点变成多分钟指数退避挂起)。两值都设时 `CC_MAX_RETRIES` 胜;agent 与 chat 两路径同源。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Notebooks — `notebook_edit` + notebook-aware `read_file`(Claude-Code NotebookEdit/Read 平价)\n\nJupyter `.ipynb` 读写一对工具,补齐 cc agent 唯一缺的 Claude Code 工具。\n\n**读**:`read_file` 对 `.ipynb` 自动渲染成**紧凑单元清单**(每格 index + id + type + source,outputs 只摘要不 dump),而非原始 JSON——避免 base64 输出把上下文撑爆/被 50k 截断藏住单元。传 `raw:true` 取底层 JSON。\n\n```\nJupyter notebook — 2 cell(s), nbformat 4. Edit cells with the notebook_edit tool …\n\n── Cell 0 [code id=c1] ──\nx = 1\nprint(x)\n ⟨1 output(s) hidden⟩\n\n── Cell 1 [markdown id=m1] ──\n# Title\n```\n\n**写**:`notebook_edit` 改单个单元——\n\n| 参数 | 说明 |\n|---|---|\n| `path` | `.ipynb` 路径(必填) |\n| `edit_mode` | `replace`(默认)/ `insert` / `delete` |\n| `cell_id` / `cell_index` | 目标定位(cell_id 优先;从渲染清单里读 id) |\n| `cell_type` | `code` / `markdown`——`insert` 必填 |\n| `new_source` | 新单元源文本(replace/insert 必填) |\n\n- **replace**:覆盖目标格 source;若是 code 格,顺带清空 `outputs` + `execution_count`(旧输出会误导)。\n- **insert**:在目标格**之后**插新格(无目标→插到**顶部**),自动生成 cell id。\n- **delete**:移除目标格。\n- 源以保留行尾 `\\n` 的行数组存盘,1-space 缩进 + 尾换行重序列化(nbformat 习惯)。\n\n**典型流程**:`read_file notebook.ipynb`(看到各格 id)→ `notebook_edit {path, cell_id, new_source}`。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task`/`Agent` | `spawn_sub_agent` | 子 agent 类型 | `Agent(explorer)`、`Agent(executor,design)` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- **命名参数**(Claude-Code 2.1.178):`Tool(param:value)` 匹配任一具名实参,\n 如 `Bash(command:rm*)`、`mcp__db__query(table:users)`。\n- **子 agent 类型**(Claude-Code 2.1.186):`Agent(type)` / `Agent(typeA,typeB)`\n 按 `spawn_sub_agent` 调用的类型(`profile` / 具名 `agent` / `role`,取其一)匹配——\n 逗号列表任一命中即可、类型名大小写不敏感、支持 `*`/`?` glob。从 Claude-Code\n `settings.json` 直接移植的 `Agent(…)` 规则因此会被执行而非静默忽略。\n (`Task(profile:explorer)` 命名参数写法仍可用。)\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n### `autoMode.classifyAllShell`(Claude-Code 2.1.193 平价)\n\nshell-policy 默认给一组**验证类命令**(`npm test` / `npm run lint` / `npm run build` /\n单文件 Playwright / `rg`)走**快车道 ALLOW**(风险 LOW,自动放行不弹确认)。在\n`.claude/settings.json` 里设 `{ \"autoMode\": { \"classifyAllShell\": true } }` 后,这组内置\n白名单也被**降级为 WARN**(风险 MEDIUM)→ 经 ApprovalGate 确认,**任何 shell 命令都不再静默自跑**。\n\n- 只影响**内置便利白名单**;硬黑名单(`rm`/`curl`/destructive git/disk)仍 **DENY**,未分类命令本就是 WARN。\n- 用户显式的 settings `allow` 规则(`Bash(npm run test:*)`)是**另一层有意预授权**,不受此开关影响。\n- 层级与其它设置一致(`~/.claude` < 项目 < `.local` < `--settings`,最近层胜);未设 = 关。\n- 无 ApprovalGate 时(纯 headless 无 gate)降级为 WARN 仍是 allowed——开关的\"牙齿\"来自交互档位的确认。\n\n## Credential 读取保护(Claude-Code 2.1.189 `sandbox.credentials` 平价)\n\ncc 不做 OS 沙箱,但同样的意图在**工具层**落地:Agent 不应把用户的密钥静默吸进模型上下文\n(可能被回显、写进转录、或被后续工具外泄)。两条读取路径被**确认优先 / headless fail-closed**\n拦截(与 sensitive-file **写**保护同款):\n\n- **`read_file`** 指向凭据文件:`.env`(及 `.env.local` / `.env.production` 等真实变体——但\n `.env.example` / `.env.sample` / `.env.template` 等模板**不拦**)、`~/.aws/credentials`、\n `~/.kube/config`、`~/.docker/config.json`、`~/.config/gh/hosts.yml`、`.npmrc` / `.netrc` /\n `.git-credentials` / `.pgpass`、私钥与证书材料(`*.pem` / `*.key` / `*.pfx` / `*.p12` /\n `*.jks` / `id_rsa` 等;`*.pub` / `*.crt` 公钥**不拦**)、`secrets.{json,yaml}`。\n- **`run_shell`** 读凭据:用 `cat` / `type` / `Get-Content` / `head` … 读上述凭据文件;\n `echo $ANTHROPIC_API_KEY` / `printf \"%s\" \"$AWS_SECRET_ACCESS_KEY\"` / `echo %GITHUB_TOKEN%` /\n `Write-Host $env:DB_PASSWORD` / `printenv OPENAI_API_KEY` 这类**打印密钥型环境变量**;\n 以及 `printenv` / `env` / `Get-ChildItem env:` 这类**全量 env dump**。复合命令的**每一段**\n 都查(`echo hi && cat .env` 也会被拦)。仅**消费**密钥而不回显的命令(`mytool --token $API_TOKEN`)\n 不拦——值不会落进输出。\n\n旁路与开关:\n\n- **显式 settings `allow` 规则**(`Read(./**)` / `Bash(cat:*)` 等)或交互确认 `ask` 已通过 →\n 预授权,不再拦。\n- 交互模式弹确认(一次按键);**headless 无确认器 → fail-closed 拒绝**,报\n `[Credential Guard] … requires confirmation — denied`。\n- `CC_CREDENTIAL_GUARD=0`(/ `false` / `no` / `off`)整体关闭。\n- **有意分歧**:`--safe-mode` **不**关闭本保护——和权限 deny 规则一样,这是**安全面**而非\n 自定义项,\"裸跑排查自定义\"绝不能放宽 Agent 能读到的密钥。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `Bash,PowerShell` 逗号 / `*` 通配 / `/regex/`(管道与逗号可混用,空段忽略)。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "f79d5a181ea41fb46f8b1a3e6209c2aff63a97f7": {"t": 1785754082.8540447, "n": 63284, "h": "42a48f6cba5d2284", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | — |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Notebooks — `notebook_edit` + notebook-aware `read_file`(Claude-Code NotebookEdit/Read 平价)\n\nJupyter `.ipynb` 读写一对工具,补齐 cc agent 唯一缺的 Claude Code 工具。\n\n**读**:`read_file` 对 `.ipynb` 自动渲染成**紧凑单元清单**(每格 index + id + type + source,outputs 只摘要不 dump),而非原始 JSON——避免 base64 输出把上下文撑爆/被 50k 截断藏住单元。传 `raw:true` 取底层 JSON。\n\n```\nJupyter notebook — 2 cell(s), nbformat 4. Edit cells with the notebook_edit tool …\n\n── Cell 0 [code id=c1] ──\nx = 1\nprint(x)\n ⟨1 output(s) hidden⟩\n\n── Cell 1 [markdown id=m1] ──\n# Title\n```\n\n**写**:`notebook_edit` 改单个单元——\n\n| 参数 | 说明 |\n|---|---|\n| `path` | `.ipynb` 路径(必填) |\n| `edit_mode` | `replace`(默认)/ `insert` / `delete` |\n| `cell_id` / `cell_index` | 目标定位(cell_id 优先;从渲染清单里读 id) |\n| `cell_type` | `code` / `markdown`——`insert` 必填 |\n| `new_source` | 新单元源文本(replace/insert 必填) |\n\n- **replace**:覆盖目标格 source;若是 code 格,顺带清空 `outputs` + `execution_count`(旧输出会误导)。\n- **insert**:在目标格**之后**插新格(无目标→插到**顶部**),自动生成 cell id。\n- **delete**:移除目标格。\n- 源以保留行尾 `\\n` 的行数组存盘,1-space 缩进 + 尾换行重序列化(nbformat 习惯)。\n\n**典型流程**:`read_file notebook.ipynb`(看到各格 id)→ `notebook_edit {path, cell_id, new_source}`。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task`/`Agent` | `spawn_sub_agent` | 子 agent 类型 | `Agent(explorer)`、`Agent(executor,design)` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- **命名参数**(Claude-Code 2.1.178):`Tool(param:value)` 匹配任一具名实参,\n 如 `Bash(command:rm*)`、`mcp__db__query(table:users)`。\n- **子 agent 类型**(Claude-Code 2.1.186):`Agent(type)` / `Agent(typeA,typeB)`\n 按 `spawn_sub_agent` 调用的类型(`profile` / 具名 `agent` / `role`,取其一)匹配——\n 逗号列表任一命中即可、类型名大小写不敏感、支持 `*`/`?` glob。从 Claude-Code\n `settings.json` 直接移植的 `Agent(…)` 规则因此会被执行而非静默忽略。\n (`Task(profile:explorer)` 命名参数写法仍可用。)\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n### `autoMode.classifyAllShell`(Claude-Code 2.1.193 平价)\n\nshell-policy 默认给一组**验证类命令**(`npm test` / `npm run lint` / `npm run build` /\n单文件 Playwright / `rg`)走**快车道 ALLOW**(风险 LOW,自动放行不弹确认)。在\n`.claude/settings.json` 里设 `{ \"autoMode\": { \"classifyAllShell\": true } }` 后,这组内置\n白名单也被**降级为 WARN**(风险 MEDIUM)→ 经 ApprovalGate 确认,**任何 shell 命令都不再静默自跑**。\n\n- 只影响**内置便利白名单**;硬黑名单(`rm`/`curl`/destructive git/disk)仍 **DENY**,未分类命令本就是 WARN。\n- 用户显式的 settings `allow` 规则(`Bash(npm run test:*)`)是**另一层有意预授权**,不受此开关影响。\n- 层级与其它设置一致(`~/.claude` < 项目 < `.local` < `--settings`,最近层胜);未设 = 关。\n- 无 ApprovalGate 时(纯 headless 无 gate)降级为 WARN 仍是 allowed——开关的\"牙齿\"来自交互档位的确认。\n\n## Credential 读取保护(Claude-Code 2.1.189 `sandbox.credentials` 平价)\n\ncc 不做 OS 沙箱,但同样的意图在**工具层**落地:Agent 不应把用户的密钥静默吸进模型上下文\n(可能被回显、写进转录、或被后续工具外泄)。两条读取路径被**确认优先 / headless fail-closed**\n拦截(与 sensitive-file **写**保护同款):\n\n- **`read_file`** 指向凭据文件:`.env`(及 `.env.local` / `.env.production` 等真实变体——但\n `.env.example` / `.env.sample` / `.env.template` 等模板**不拦**)、`~/.aws/credentials`、\n `~/.kube/config`、`~/.docker/config.json`、`~/.config/gh/hosts.yml`、`.npmrc` / `.netrc` /\n `.git-credentials` / `.pgpass`、私钥与证书材料(`*.pem` / `*.key` / `*.pfx` / `*.p12` /\n `*.jks` / `id_rsa` 等;`*.pub` / `*.crt` 公钥**不拦**)、`secrets.{json,yaml}`。\n- **`run_shell`** 读凭据:用 `cat` / `type` / `Get-Content` / `head` … 读上述凭据文件;\n `echo $ANTHROPIC_API_KEY` / `printf \"%s\" \"$AWS_SECRET_ACCESS_KEY\"` / `echo %GITHUB_TOKEN%` /\n `Write-Host $env:DB_PASSWORD` / `printenv OPENAI_API_KEY` 这类**打印密钥型环境变量**;\n 以及 `printenv` / `env` / `Get-ChildItem env:` 这类**全量 env dump**。复合命令的**每一段**\n 都查(`echo hi && cat .env` 也会被拦)。仅**消费**密钥而不回显的命令(`mytool --token $API_TOKEN`)\n 不拦——值不会落进输出。\n\n旁路与开关:\n\n- **显式 settings `allow` 规则**(`Read(./**)` / `Bash(cat:*)` 等)或交互确认 `ask` 已通过 →\n 预授权,不再拦。\n- 交互模式弹确认(一次按键);**headless 无确认器 → fail-closed 拒绝**,报\n `[Credential Guard] … requires confirmation — denied`。\n- `CC_CREDENTIAL_GUARD=0`(/ `false` / `no` / `off`)整体关闭。\n- **有意分歧**:`--safe-mode` **不**关闭本保护——和权限 deny 规则一样,这是**安全面**而非\n 自定义项,\"裸跑排查自定义\"绝不能放宽 Agent 能读到的密钥。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `Bash,PowerShell` 逗号 / `*` 通配 / `/regex/`(管道与逗号可混用,空段忽略)。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "faceae0986d2ab10ab4c86d9a851845e22f86b37": {"t": 1785754084.4260802, "n": 62640, "h": "9c7a1ba51ddec101", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | — |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Notebooks — `notebook_edit` + notebook-aware `read_file`(Claude-Code NotebookEdit/Read 平价)\n\nJupyter `.ipynb` 读写一对工具,补齐 cc agent 唯一缺的 Claude Code 工具。\n\n**读**:`read_file` 对 `.ipynb` 自动渲染成**紧凑单元清单**(每格 index + id + type + source,outputs 只摘要不 dump),而非原始 JSON——避免 base64 输出把上下文撑爆/被 50k 截断藏住单元。传 `raw:true` 取底层 JSON。\n\n```\nJupyter notebook — 2 cell(s), nbformat 4. Edit cells with the notebook_edit tool …\n\n── Cell 0 [code id=c1] ──\nx = 1\nprint(x)\n ⟨1 output(s) hidden⟩\n\n── Cell 1 [markdown id=m1] ──\n# Title\n```\n\n**写**:`notebook_edit` 改单个单元——\n\n| 参数 | 说明 |\n|---|---|\n| `path` | `.ipynb` 路径(必填) |\n| `edit_mode` | `replace`(默认)/ `insert` / `delete` |\n| `cell_id` / `cell_index` | 目标定位(cell_id 优先;从渲染清单里读 id) |\n| `cell_type` | `code` / `markdown`——`insert` 必填 |\n| `new_source` | 新单元源文本(replace/insert 必填) |\n\n- **replace**:覆盖目标格 source;若是 code 格,顺带清空 `outputs` + `execution_count`(旧输出会误导)。\n- **insert**:在目标格**之后**插新格(无目标→插到**顶部**),自动生成 cell id。\n- **delete**:移除目标格。\n- 源以保留行尾 `\\n` 的行数组存盘,1-space 缩进 + 尾换行重序列化(nbformat 习惯)。\n\n**典型流程**:`read_file notebook.ipynb`(看到各格 id)→ `notebook_edit {path, cell_id, new_source}`。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task`/`Agent` | `spawn_sub_agent` | 子 agent 类型 | `Agent(explorer)`、`Agent(executor,design)` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- **命名参数**(Claude-Code 2.1.178):`Tool(param:value)` 匹配任一具名实参,\n 如 `Bash(command:rm*)`、`mcp__db__query(table:users)`。\n- **子 agent 类型**(Claude-Code 2.1.186):`Agent(type)` / `Agent(typeA,typeB)`\n 按 `spawn_sub_agent` 调用的类型(`profile` / 具名 `agent` / `role`,取其一)匹配——\n 逗号列表任一命中即可、类型名大小写不敏感、支持 `*`/`?` glob。从 Claude-Code\n `settings.json` 直接移植的 `Agent(…)` 规则因此会被执行而非静默忽略。\n (`Task(profile:explorer)` 命名参数写法仍可用。)\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Credential 读取保护(Claude-Code 2.1.189 `sandbox.credentials` 平价)\n\ncc 不做 OS 沙箱,但同样的意图在**工具层**落地:Agent 不应把用户的密钥静默吸进模型上下文\n(可能被回显、写进转录、或被后续工具外泄)。两条读取路径被**确认优先 / headless fail-closed**\n拦截(与 sensitive-file **写**保护同款):\n\n- **`read_file`** 指向凭据文件:`.env`(及 `.env.local` / `.env.production` 等真实变体——但\n `.env.example` / `.env.sample` / `.env.template` 等模板**不拦**)、`~/.aws/credentials`、\n `~/.kube/config`、`~/.docker/config.json`、`~/.config/gh/hosts.yml`、`.npmrc` / `.netrc` /\n `.git-credentials` / `.pgpass`、私钥与证书材料(`*.pem` / `*.key` / `*.pfx` / `*.p12` /\n `*.jks` / `id_rsa` 等;`*.pub` / `*.crt` 公钥**不拦**)、`secrets.{json,yaml}`。\n- **`run_shell`** 读凭据:用 `cat` / `type` / `Get-Content` / `head` … 读上述凭据文件;\n `echo $ANTHROPIC_API_KEY` / `printf \"%s\" \"$AWS_SECRET_ACCESS_KEY\"` / `echo %GITHUB_TOKEN%` /\n `Write-Host $env:DB_PASSWORD` / `printenv OPENAI_API_KEY` 这类**打印密钥型环境变量**;\n 以及 `printenv` / `env` / `Get-ChildItem env:` 这类**全量 env dump**。复合命令的**每一段**\n 都查(`echo hi && cat .env` 也会被拦)。仅**消费**密钥而不回显的命令(`mytool --token $API_TOKEN`)\n 不拦——值不会落进输出。\n\n旁路与开关:\n\n- **显式 settings `allow` 规则**(`Read(./**)` / `Bash(cat:*)` 等)或交互确认 `ask` 已通过 →\n 预授权,不再拦。\n- 交互模式弹确认(一次按键);**headless 无确认器 → fail-closed 拒绝**,报\n `[Credential Guard] … requires confirmation — denied`。\n- `CC_CREDENTIAL_GUARD=0`(/ `false` / `no` / `off`)整体关闭。\n- **有意分歧**:`--safe-mode` **不**关闭本保护——和权限 deny 规则一样,这是**安全面**而非\n 自定义项,\"裸跑排查自定义\"绝不能放宽 Agent 能读到的密钥。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `Bash,PowerShell` 逗号 / `*` 通配 / `/regex/`(管道与逗号可混用,空段忽略)。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "f93f58df5170f7517a14de8bd504df9656b1bedc": {"t": 1785754085.9294016, "n": 62602, "h": "68cc19ab039932c4", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | — |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Notebooks — `notebook_edit` + notebook-aware `read_file`(Claude-Code NotebookEdit/Read 平价)\n\nJupyter `.ipynb` 读写一对工具,补齐 cc agent 唯一缺的 Claude Code 工具。\n\n**读**:`read_file` 对 `.ipynb` 自动渲染成**紧凑单元清单**(每格 index + id + type + source,outputs 只摘要不 dump),而非原始 JSON——避免 base64 输出把上下文撑爆/被 50k 截断藏住单元。传 `raw:true` 取底层 JSON。\n\n```\nJupyter notebook — 2 cell(s), nbformat 4. Edit cells with the notebook_edit tool …\n\n── Cell 0 [code id=c1] ──\nx = 1\nprint(x)\n ⟨1 output(s) hidden⟩\n\n── Cell 1 [markdown id=m1] ──\n# Title\n```\n\n**写**:`notebook_edit` 改单个单元——\n\n| 参数 | 说明 |\n|---|---|\n| `path` | `.ipynb` 路径(必填) |\n| `edit_mode` | `replace`(默认)/ `insert` / `delete` |\n| `cell_id` / `cell_index` | 目标定位(cell_id 优先;从渲染清单里读 id) |\n| `cell_type` | `code` / `markdown`——`insert` 必填 |\n| `new_source` | 新单元源文本(replace/insert 必填) |\n\n- **replace**:覆盖目标格 source;若是 code 格,顺带清空 `outputs` + `execution_count`(旧输出会误导)。\n- **insert**:在目标格**之后**插新格(无目标→插到**顶部**),自动生成 cell id。\n- **delete**:移除目标格。\n- 源以保留行尾 `\\n` 的行数组存盘,1-space 缩进 + 尾换行重序列化(nbformat 习惯)。\n\n**典型流程**:`read_file notebook.ipynb`(看到各格 id)→ `notebook_edit {path, cell_id, new_source}`。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task`/`Agent` | `spawn_sub_agent` | 子 agent 类型 | `Agent(explorer)`、`Agent(executor,design)` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- **命名参数**(Claude-Code 2.1.178):`Tool(param:value)` 匹配任一具名实参,\n 如 `Bash(command:rm*)`、`mcp__db__query(table:users)`。\n- **子 agent 类型**(Claude-Code 2.1.186):`Agent(type)` / `Agent(typeA,typeB)`\n 按 `spawn_sub_agent` 调用的类型(`profile` / 具名 `agent` / `role`,取其一)匹配——\n 逗号列表任一命中即可、类型名大小写不敏感、支持 `*`/`?` glob。从 Claude-Code\n `settings.json` 直接移植的 `Agent(…)` 规则因此会被执行而非静默忽略。\n (`Task(profile:explorer)` 命名参数写法仍可用。)\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Credential 读取保护(Claude-Code 2.1.189 `sandbox.credentials` 平价)\n\ncc 不做 OS 沙箱,但同样的意图在**工具层**落地:Agent 不应把用户的密钥静默吸进模型上下文\n(可能被回显、写进转录、或被后续工具外泄)。两条读取路径被**确认优先 / headless fail-closed**\n拦截(与 sensitive-file **写**保护同款):\n\n- **`read_file`** 指向凭据文件:`.env`(及 `.env.local` / `.env.production` 等真实变体——但\n `.env.example` / `.env.sample` / `.env.template` 等模板**不拦**)、`~/.aws/credentials`、\n `~/.kube/config`、`~/.docker/config.json`、`~/.config/gh/hosts.yml`、`.npmrc` / `.netrc` /\n `.git-credentials` / `.pgpass`、私钥与证书材料(`*.pem` / `*.key` / `*.pfx` / `*.p12` /\n `*.jks` / `id_rsa` 等;`*.pub` / `*.crt` 公钥**不拦**)、`secrets.{json,yaml}`。\n- **`run_shell`** 读凭据:用 `cat` / `type` / `Get-Content` / `head` … 读上述凭据文件;\n `echo $ANTHROPIC_API_KEY` / `printf \"%s\" \"$AWS_SECRET_ACCESS_KEY\"` / `echo %GITHUB_TOKEN%` /\n `Write-Host $env:DB_PASSWORD` / `printenv OPENAI_API_KEY` 这类**打印密钥型环境变量**;\n 以及 `printenv` / `env` / `Get-ChildItem env:` 这类**全量 env dump**。复合命令的**每一段**\n 都查(`echo hi && cat .env` 也会被拦)。仅**消费**密钥而不回显的命令(`mytool --token $API_TOKEN`)\n 不拦——值不会落进输出。\n\n旁路与开关:\n\n- **显式 settings `allow` 规则**(`Read(./**)` / `Bash(cat:*)` 等)或交互确认 `ask` 已通过 →\n 预授权,不再拦。\n- 交互模式弹确认(一次按键);**headless 无确认器 → fail-closed 拒绝**,报\n `[Credential Guard] … requires confirmation — denied`。\n- `CC_CREDENTIAL_GUARD=0`(/ `false` / `no` / `off`)整体关闭。\n- **有意分歧**:`--safe-mode` **不**关闭本保护——和权限 deny 规则一样,这是**安全面**而非\n 自定义项,\"裸跑排查自定义\"绝不能放宽 Agent 能读到的密钥。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "8c41fe06cd06619048a6e05a4fde881b831b3ed5": {"t": 1785754087.4643836, "n": 61200, "h": "88d42a501f4f5be4", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | — |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Notebooks — `notebook_edit` + notebook-aware `read_file`(Claude-Code NotebookEdit/Read 平价)\n\nJupyter `.ipynb` 读写一对工具,补齐 cc agent 唯一缺的 Claude Code 工具。\n\n**读**:`read_file` 对 `.ipynb` 自动渲染成**紧凑单元清单**(每格 index + id + type + source,outputs 只摘要不 dump),而非原始 JSON——避免 base64 输出把上下文撑爆/被 50k 截断藏住单元。传 `raw:true` 取底层 JSON。\n\n```\nJupyter notebook — 2 cell(s), nbformat 4. Edit cells with the notebook_edit tool …\n\n── Cell 0 [code id=c1] ──\nx = 1\nprint(x)\n ⟨1 output(s) hidden⟩\n\n── Cell 1 [markdown id=m1] ──\n# Title\n```\n\n**写**:`notebook_edit` 改单个单元——\n\n| 参数 | 说明 |\n|---|---|\n| `path` | `.ipynb` 路径(必填) |\n| `edit_mode` | `replace`(默认)/ `insert` / `delete` |\n| `cell_id` / `cell_index` | 目标定位(cell_id 优先;从渲染清单里读 id) |\n| `cell_type` | `code` / `markdown`——`insert` 必填 |\n| `new_source` | 新单元源文本(replace/insert 必填) |\n\n- **replace**:覆盖目标格 source;若是 code 格,顺带清空 `outputs` + `execution_count`(旧输出会误导)。\n- **insert**:在目标格**之后**插新格(无目标→插到**顶部**),自动生成 cell id。\n- **delete**:移除目标格。\n- 源以保留行尾 `\\n` 的行数组存盘,1-space 缩进 + 尾换行重序列化(nbformat 习惯)。\n\n**典型流程**:`read_file notebook.ipynb`(看到各格 id)→ `notebook_edit {path, cell_id, new_source}`。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task`/`Agent` | `spawn_sub_agent` | 子 agent 类型 | `Agent(explorer)`、`Agent(executor,design)` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- **命名参数**(Claude-Code 2.1.178):`Tool(param:value)` 匹配任一具名实参,\n 如 `Bash(command:rm*)`、`mcp__db__query(table:users)`。\n- **子 agent 类型**(Claude-Code 2.1.186):`Agent(type)` / `Agent(typeA,typeB)`\n 按 `spawn_sub_agent` 调用的类型(`profile` / 具名 `agent` / `role`,取其一)匹配——\n 逗号列表任一命中即可、类型名大小写不敏感、支持 `*`/`?` glob。从 Claude-Code\n `settings.json` 直接移植的 `Agent(…)` 规则因此会被执行而非静默忽略。\n (`Task(profile:explorer)` 命名参数写法仍可用。)\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "526e8fea0dcb796e5c26b09b74ea9e306b58b8b6": {"t": 1785754089.4535923, "n": 60798, "h": "bc7da2646a709018", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | — |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Notebooks — `notebook_edit` + notebook-aware `read_file`(Claude-Code NotebookEdit/Read 平价)\n\nJupyter `.ipynb` 读写一对工具,补齐 cc agent 唯一缺的 Claude Code 工具。\n\n**读**:`read_file` 对 `.ipynb` 自动渲染成**紧凑单元清单**(每格 index + id + type + source,outputs 只摘要不 dump),而非原始 JSON——避免 base64 输出把上下文撑爆/被 50k 截断藏住单元。传 `raw:true` 取底层 JSON。\n\n```\nJupyter notebook — 2 cell(s), nbformat 4. Edit cells with the notebook_edit tool …\n\n── Cell 0 [code id=c1] ──\nx = 1\nprint(x)\n ⟨1 output(s) hidden⟩\n\n── Cell 1 [markdown id=m1] ──\n# Title\n```\n\n**写**:`notebook_edit` 改单个单元——\n\n| 参数 | 说明 |\n|---|---|\n| `path` | `.ipynb` 路径(必填) |\n| `edit_mode` | `replace`(默认)/ `insert` / `delete` |\n| `cell_id` / `cell_index` | 目标定位(cell_id 优先;从渲染清单里读 id) |\n| `cell_type` | `code` / `markdown`——`insert` 必填 |\n| `new_source` | 新单元源文本(replace/insert 必填) |\n\n- **replace**:覆盖目标格 source;若是 code 格,顺带清空 `outputs` + `execution_count`(旧输出会误导)。\n- **insert**:在目标格**之后**插新格(无目标→插到**顶部**),自动生成 cell id。\n- **delete**:移除目标格。\n- 源以保留行尾 `\\n` 的行数组存盘,1-space 缩进 + 尾换行重序列化(nbformat 习惯)。\n\n**典型流程**:`read_file notebook.ipynb`(看到各格 id)→ `notebook_edit {path, cell_id, new_source}`。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "560d2afa03369cd10bf8731dbaf72f93d872f655": {"t": 1785754092.2423718, "n": 59692, "h": "70e41bbee5ab5dd0", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## 流式韧性与 Prompt 缓存(`cc agent` + `cc chat`/`cc ask` 一致)\n\n两套流式实现(agent-core 给 `cc agent`、chat-core 给 `cc chat`/`cc ask`)在以下维度行为一致;可重试分类器走共享 `src/lib/stream-retry.js`(单一真相源)。\n\n**流式韧性**(API 接受连接后中途静默 / 瞬态断流):\n\n| 行为 | 阈值(默认) | 调节 |\n|---|---|---|\n| 静默提示 \"⏳ waiting for API response\"(stderr,不中断) | 20s | agent:无单独 env(常量 `STREAM_STALL_MS`);chat:`CC_CHAT_STALL_HINT_MS` |\n| 硬不活动超时 → 取消 + **重试**(retryable 错) | 180s | agent:`config.llm.streamStallTimeoutMs`(0=关);chat:`CC_CHAT_STALL_MS` |\n| 瞬态连接 drop 自动重试(ECONNRESET / fetch failed / socket hangup 等) | 最多 2 次 | — |\n\n- **重试只在“零 token 已输出”时发生**——一旦有 token 流到用户,再发会重复可见输出,故改为保留 partial(agent)/原样抛(chat)。HTTP/auth 错(4xx/5xx)与 180s stall abort **不**重试。\n- 180s 硬超时对慢首 token 的本地模型足够宽松;云端 provider 长时间静默≈死连接,自动重发恢复。\n\n**Anthropic Prompt 缓存**(`cc agent`,默认开):system prompt + ~18 个工具 schema 是每轮重发的大而稳定前缀。cc 给最后一个 tool + system 块各打一个 `cache_control:{type:\"ephemeral\"}` 断点 → Anthropic 从缓存供该前缀(≈10% input 成本、更低延迟)。前缀低于模型最小可缓存尺寸时 Anthropic 自动忽略,**永远安全**。\n\n- **关闭**:`CC_PROMPT_CACHE=0`(env)或 `options.promptCaching:false`(给会拒该字段的自定义 anthropic 兼容网关)。关闭时请求字节级回到原样。\n\n**缓存 token 成本核算**(`cc cost` / `cc session usage` / `/cost`):Anthropic(`cache_read_input_tokens`/`cache_creation_input_tokens`)与 OpenAI-兼容 provider(OpenAI/volcengine 的 `prompt_tokens_details.cached_tokens`、DeepSeek 的 `prompt_cache_hit_tokens`,均**含在** `prompt_tokens` 内 → 拆出避免重复计价)的缓存 token 被单独捕获并按各 provider 缓存读倍率计价(Anthropic ≈0.1×、OpenAI ≈0.5×、DeepSeek ≈0.25×;写 ≈1.25× 仅 Anthropic)。无缓存时与旧行为字节级一致。\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "56c59ad362d7bd2d4b77976ce8e80e77d733f05e": {"t": 1785754094.2120368, "n": 58204, "h": "dc242c3694ae40d3", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## PDH Bridge — 自动连接安卓 App 的设备能力 MCP server (module 101)\n\n第四条 MCP 源,把 IDE 桥接的思路搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\"(暴露\n`collect_system_data`/`collect_app_data`/`salvage_app_data`/`collect_files` 等**采集工具**),并把端点写进\nlockfile。App 启动时拉起内置 cc 并注入 env,`cc agent` 自动发现、当保留名 `pdh` 的 server 连上,工具即\n`mcp__pdh__collect_system_data` 等。**让 agent 第一次能主动指挥设备采集自己的数据**(进程模型从\"App 把 cc 当\n一次性子进程\"反转为\"App = server, cc = client\")。设计见 `docs/design/modules/101_个人数据IDE桥接方案.md`。\n**Phase 0(发现层+协议)+ Phase 1(L1–L4 采集工具)已落地**。\n\n**发现两条路径**(`src/lib/pdh-bridge.js`):\n\n- **env 直连(主)**:App 给内置 cc 注入 `CHAINLESSCHAIN_PDH_PORT`(+ 可选 `CHAINLESSCHAIN_PDH_TOKEN`)→ 直接锁定该\n port 的锁,免扫描。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/pdh-bridge/*.json`,取**最新存活**锁(一台设备只有一个 PDH\n server,无 IDE 那种 workspace 概念)。\n\n**Lockfile**(`~/.chainlesschain/pdh-bridge/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"kind\": \"pdh-bridge\",\n \"version\": 1,\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:18510/mcp\",\n \"port\": 18510,\n \"device\": \"android\",\n \"appUid\": 10367,\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `http`/`https`/`sse`(暂不支持 `ws`);连接注入\n`Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。该 server 标记 `longRunning`——\n采集工具可在 `assist_required`(需你先在目标 App 登录/暖热)时阻塞等你完成再重试,免疫单次调用超时。\n\n```bash\nchainlesschain agent \"采集本机系统数据入库\" # 在 App 内置终端内自动连接 → mcp__pdh__collect_system_data\nchainlesschain agent --pdh \"列出能采集哪些数据\" # 强制启用(外部终端手动接)→ mcp__pdh__list_collectors\nchainlesschain agent --no-pdh \"...\" # 禁用 PDH 自动连接\n\nchainlesschain pdh list # 列出发现到的 PDH server(token 不显)\nchainlesschain pdh status # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain pdh doctor # 解释发现为何成功/失败(无锁/stale/非 localhost/…)\n```\n\n**保留名 `pdh`**:若你已 `cc mcp add pdh` 自建同名 server,用户显式注册优先,PDH 自动发现让位。\n\n**6 个设备工具**:`pdh_ping`(存活)/ `collect_files`(L1)/ `collect_system_data`(L2 联系人+应用,免 root/登录)/\n`collect_app_data`(L3,7 源 weibo/bilibili/12306/douyin/xhs/toutiao/kuaishou,无凭证→`assist_required`)/\n`salvage_app_data`(L4 root 内存打捞)/ `list_collectors`。采集衔接:Kotlin 产快照 → spawn\n`cc hub sync-adapter ` 入库。完整安装/协议/排错见用户文档 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "2cd6939d4f66dbffcb862605af1bada94b6e3a08": {"t": 1785754095.9748063, "n": 55925, "h": "c88c73ce01a270bf", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 自动识别消息里的图片路径(Claude-Code 平价)\n\n除了显式 `--image`,**消息里直接写一个本地图片路径**就会被自动当成视觉输入读图(和 Claude Code 一致),无需任何参数:\n\n```bash\necho \"描述这张图 ./shot.png\" | chainlesschain agent --input-format stream-json --output-format stream-json\n# 交互 REPL 里同理:直接打 \"描述 C:\\Users\\me\\shot.png\"\n```\n\n- **生效范围**:headless stream-json 输入、交互 REPL,以及 **VS Code / JetBrains 聊天面板**(面板就是经 `cc agent --input-format stream-json` 驱动,因此零改插件即生效)。\n- **识别规则**:只把**磁盘上真实存在**且扩展名受支持(`png/jpg/jpeg/gif/webp`)的 token 当图片;识别后把路径文本从消息里剥掉。不存在的路径、不支持的扩展名、`http(s)://` / `data:` 远程地址都**不会**被附图。\n- **路径形式**:`C:\\Users\\…`、`C:/Users/…`、Git-Bash 形 `/c/Users/…`、`~/…`、以及 IDE 拖拽给的 `file:///C:/Users/…`(含 `%20` 转义)都能识别。\n- **自动切视觉模型**:命中图片的回合会**切到视觉模型**(`config.llm.visionModel`,未配则用内置默认 `doubao-seed-2-0-lite-260215`;只换模型,provider/key/baseUrl 不变),stderr 打印 `[image] N attached → vision model X`。\n- **开关**:`CC_AUTO_IMAGE=0` 关闭自动识别(仍可用显式 `--image` / 粘贴)。\n\n> 配套:面板里 **Ctrl/Cmd+V 粘贴截图**走的是另一条(粘贴真图数据)路径;视觉模型可在 `cc config set llm.visionModel ` 或编辑器的「配置图片识别模型」入口里改。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "a6998ac2aa3a429b12f5fb2a9d80052cbfaf15b4": {"t": 1785754099.6068144, "n": 54922, "h": "8f09969d52d84e5a", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### 项目 `.mcp.json` — 团队共享配置(opt-in,默认关)\n\n对标 Claude Code 的项目级 `.mcp.json`:在 git 项目根(从 cwd 向上找,与 `.claude`\n各层一致)和 cwd 放一个 `.mcp.json`(`{ \"mcpServers\": { ... } }`),`cc agent` 合并两处\n(同名时 **cwd 就近覆盖根**)。**安全默认关** —— 签入仓库的 `.mcp.json` 能 spawn 任意\n命令,克隆任何仓库就跑等于供应链风险,故必须**显式 opt-in**:\n\n```bash\nchainlesschain agent --project-mcp -p \"...\" # 仅本次运行加载项目 .mcp.json\nCC_PROJECT_MCP=1 chainlesschain agent -p \"...\" # 等价(整 shell 生效)\n```\n\n精度:`--mcp-config` 与注册 server **同名时优先**(先连者赢);加载顺序排在 IDE 自动发现\n之前;`--strict-mcp-config` 下完全跳过(只用 `--mcp-config`);未 opt-in 时彻底 no-op。\n与 Claude Code 不同,暂无逐 server 审批弹窗 —— env/flag 的 opt-in 即当前的安全闸。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "309655f036c9186b64ae2ea4979821d9c4a39988": {"t": 1785754101.1136813, "n": 54302, "h": "57a8c8df249948d6", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答 + 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条 agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config` + 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "d3fe0730d4b59481f672cdb4eeff7b2fcb12e586": {"t": 1785754102.6461663, "n": 54308, "h": "e8f5dd517ce9234d", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\nnum_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ------------------- | --------- | ---------------------------------------------------------------- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"llm\": {\n \"pricing\": {\n \"openai\": [{ \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 }],\n \"myprovider\": [{ \"match\": \"custom\", \"in\": 2, \"out\": 8 }],\n },\n },\n}\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答\n\n- 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条\n agent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ```代码围栏里也能提取(取首个配平`{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\ncompressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n| -------------------------- | -------------------------------------------------- |\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config`\n\n- 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见\n `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n\n```jsonc\n{\n \"ide\": \"vscode\",\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:53690/sse\",\n \"port\": 53690,\n \"workspaceFolders\": [\"/abs/ws\"],\n \"token\": \"\",\n \"pid\": 12345,\n \"started_at\": 1718000000000,\n}\n```\n\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\nuser requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\", // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n },\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\nresource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ```自动升级围栏)+ compact 标记 + token 汇总。`last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n| ----------------------------- | ------------------------------------------------- |\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ----------- | -------------------------------- | ------------------ | ------------------------------------------ |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 }]\n }\n ],\n \"PostToolUse\": [\n {\n \"matcher\": \"Edit|Write\",\n \"hooks\": [{ \"type\": \"command\", \"command\": \"./fmt.sh\" }]\n }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| ------------------------------------------------------------------------------- | ---------------------------------------------- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON) | continue(放行;stdout 可作 `additionalContext`) |\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent) |\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\n\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\n\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n\n见正文各节。\n\n### 6. 技术架构\n\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n\n见正文使用场景。\n\n### 9. 竞品对比\n\n见正文对比(如有)。\n\n### 10. 配置参考\n\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "79504b635b71c03fc49776a3541439be1fcc365d": {"t": 1785754104.19428, "n": 50666, "h": "eb8936750470c1b7", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\n num_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ---- | ---- | ---- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{ \"llm\": { \"pricing\": {\n \"openai\": [ { \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 } ],\n \"myprovider\": [ { \"match\": \"custom\", \"in\": 2, \"out\": 8 } ]\n} } }\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答\n+ 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条\nagent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ``` 代码围栏里也能提取(取首个配平 `{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\n compressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n|---|---|\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config`\n+ 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n```jsonc\n{ \"ide\":\"vscode\", \"transport\":\"sse\", \"url\":\"http://127.0.0.1:53690/sse\",\n \"port\":53690, \"workspaceFolders\":[\"/abs/ws\"], \"token\":\"\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` / `@terminal` at-mention(显式版)**:除了上面\"每轮自动共享\"的\n 隐式注入,你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics`\n 把**整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件),或写 `@terminal` 把**最近 5 条终端命令 + 输出**(每条截断 2000 字符,比每轮\n 自动注入的 2 条/800 字符更全)拼进来。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会\n 误匹配),在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,\n 不写持久化)。未连 IDE / 无选区 / 无问题 / 无终端记录时,`@mention` 原样留在文中并打印一条 `@ide:`\n 提示。与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\n user requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\" // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n }\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\n resource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ``` 自动升级围栏)+ compact 标记 + token 汇总。\n `last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n|---|---|\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ---- | ----------- | ------------------ | -- |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n { \"matcher\": \"Bash\",\n \"hooks\": [ { \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 } ] }\n ],\n \"PostToolUse\": [\n { \"matcher\": \"Edit|Write\", \"hooks\": [ { \"type\": \"command\", \"command\": \"./fmt.sh\" } ] }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| --------- | ---- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON)| continue(放行;stdout 可作 `additionalContext`)|\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent)|\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| ---- | ------ | ---- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| ----------------------- | ------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| ------------ | ----------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n见正文各节。\n\n### 6. 技术架构\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n见正文使用场景。\n\n### 9. 竞品对比\n见正文对比(如有)。\n\n### 10. 配置参考\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "80f99e2ea348ed40f5c1007c625e1b0572cc95bd": {"t": 1785754105.6884866, "n": 50568, "h": "7063d1e66d4cd2ea", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\n num_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ---- | ---- | ---- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{ \"llm\": { \"pricing\": {\n \"openai\": [ { \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 } ],\n \"myprovider\": [ { \"match\": \"custom\", \"in\": 2, \"out\": 8 } ]\n} } }\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答\n+ 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条\nagent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ``` 代码围栏里也能提取(取首个配平 `{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\n compressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n|---|---|\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config`\n+ 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n```jsonc\n{ \"ide\":\"vscode\", \"transport\":\"sse\", \"url\":\"http://127.0.0.1:53690/sse\",\n \"port\":53690, \"workspaceFolders\":[\"/abs/ws\"], \"token\":\"\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` at-mention(显式版)**:除了上面\"每轮自动共享\"的隐式注入,\n 你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics` 把\n **整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件)。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会误匹配),\n 在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,不写\n 持久化)。未连 IDE / 无选区 / 无问题时,`@mention` 原样留在文中并打印一条 `@ide:` 提示。\n 与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\n user requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\" // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n }\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\n resource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ``` 自动升级围栏)+ compact 标记 + token 汇总。\n `last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **交互 REPL `/theme`**(Claude-Code `/theme` 平价) — 切换 REPL 配色:`/theme` 列出可选项 +\n 当前项,`/theme ` 切换并持久化(config `cli.theme`,下次启动沿用)。cc REPL\n 是 chalk-on-readline 而非全屏 TUI,故主题控制两件可见的事:`mono` 把 `chalk.level` 置 0**完全去色**\n (适合配色不可读的终端或纯文本抓取),`light` 用**蓝色提示符**(绿色在浅底不易读)、`dark`/`auto`\n 用绿色。启动时先存基线色深再应用主题,`mono→auto` 可逆。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n|---|---|\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ---- | ----------- | ------------------ | -- |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n { \"matcher\": \"Bash\",\n \"hooks\": [ { \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 } ] }\n ],\n \"PostToolUse\": [\n { \"matcher\": \"Edit|Write\", \"hooks\": [ { \"type\": \"command\", \"command\": \"./fmt.sh\" } ] }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| --------- | ---- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON)| continue(放行;stdout 可作 `additionalContext`)|\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent)|\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| ---- | ------ | ---- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| ----------------------- | ------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| ------------ | ----------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n见正文各节。\n\n### 6. 技术架构\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n见正文使用场景。\n\n### 9. 竞品对比\n见正文对比(如有)。\n\n### 10. 配置参考\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "c9ae40172678d7c5ffb3b72ffe8f5c28865bd165": {"t": 1785754107.2687414, "n": 50231, "h": "30803df94b33f77d", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\n num_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ---- | ---- | ---- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{ \"llm\": { \"pricing\": {\n \"openai\": [ { \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 } ],\n \"myprovider\": [ { \"match\": \"custom\", \"in\": 2, \"out\": 8 } ]\n} } }\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答\n+ 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条\nagent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ``` 代码围栏里也能提取(取首个配平 `{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\n compressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n|---|---|\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config`\n+ 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n```jsonc\n{ \"ide\":\"vscode\", \"transport\":\"sse\", \"url\":\"http://127.0.0.1:53690/sse\",\n \"port\":53690, \"workspaceFolders\":[\"/abs/ws\"], \"token\":\"\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` at-mention(显式版)**:除了上面\"每轮自动共享\"的隐式注入,\n 你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics` 把\n **整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件)。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会误匹配),\n 在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,不写\n 持久化)。未连 IDE / 无选区 / 无问题时,`@mention` 原样留在文中并打印一条 `@ide:` 提示。\n 与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\n user requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\" // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n }\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\n resource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ``` 自动升级围栏)+ compact 标记 + token 汇总。\n `last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **交互 REPL `/copy`**(Claude-Code `/copy` 平价) — 把**最后一条 assistant 回复**复制到系统剪贴板,\n 无需鼠标选中。`/copy code` 只复制最后一个**围栏代码块**。跨平台:Windows 走 PowerShell\n `Set-Clipboard`(强制 UTF-8 输入解码,非 ASCII 不乱码;`clip` 兜底)、macOS `pbcopy`、Linux 依次\n 尝试 `wl-copy`/`xclip`/`xsel`。无可用工具时给出安装提示。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n|---|---|\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ---- | ----------- | ------------------ | -- |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n { \"matcher\": \"Bash\",\n \"hooks\": [ { \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 } ] }\n ],\n \"PostToolUse\": [\n { \"matcher\": \"Edit|Write\", \"hooks\": [ { \"type\": \"command\", \"command\": \"./fmt.sh\" } ] }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| --------- | ---- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON)| continue(放行;stdout 可作 `additionalContext`)|\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent)|\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| ---- | ------ | ---- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| ----------------------- | ------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| ------------ | ----------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n见正文各节。\n\n### 6. 技术架构\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n见正文使用场景。\n\n### 9. 竞品对比\n见正文对比(如有)。\n\n### 10. 配置参考\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "53eb3d8452408e66385e9d697d63d1a190e75daa": {"t": 1785754108.7458286, "n": 49964, "h": "c96e659cee642078", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\n num_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ---- | ---- | ---- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{ \"llm\": { \"pricing\": {\n \"openai\": [ { \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 } ],\n \"myprovider\": [ { \"match\": \"custom\", \"in\": 2, \"out\": 8 } ]\n} } }\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答\n+ 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条\nagent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ``` 代码围栏里也能提取(取首个配平 `{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\n compressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n|---|---|\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config`\n+ 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n```jsonc\n{ \"ide\":\"vscode\", \"transport\":\"sse\", \"url\":\"http://127.0.0.1:53690/sse\",\n \"port\":53690, \"workspaceFolders\":[\"/abs/ws\"], \"token\":\"\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` at-mention(显式版)**:除了上面\"每轮自动共享\"的隐式注入,\n 你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics` 把\n **整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件)。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会误匹配),\n 在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,不写\n 持久化)。未连 IDE / 无选区 / 无问题时,`@mention` 原样留在文中并打印一条 `@ide:` 提示。\n 与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\n user requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\" // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n }\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\n resource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ``` 自动升级围栏)+ compact 标记 + token 汇总。\n `last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **交互 REPL `/review`**(Claude-Code `/code-review` 平价) — 在 `chainlesschain agent`\n 会话里**对当前改动做 diff-first 代码评审**,无需退出 REPL 单独跑 `cc review`。复用同一套机制:\n 收集 git diff 后跑**一轮聚焦 agent**,默认 plan 模式(只读、不改文件)输出 Markdown 报告。\n 内联参数:`/review [low|medium|high]` 力度 · `--security`(安全专项)/`--simplify`(只清理不挑 bug)·\n `--fix`(直接改工作区,自动 checkpoint 可 `cc checkpoint restore` 撤销)· 范围 `--staged`/\n `--base `/`--range `。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n|---|---|\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ---- | ----------- | ------------------ | -- |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n { \"matcher\": \"Bash\",\n \"hooks\": [ { \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 } ] }\n ],\n \"PostToolUse\": [\n { \"matcher\": \"Edit|Write\", \"hooks\": [ { \"type\": \"command\", \"command\": \"./fmt.sh\" } ] }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| --------- | ---- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON)| continue(放行;stdout 可作 `additionalContext`)|\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent)|\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| ---- | ------ | ---- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| ----------------------- | ------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| ------------ | ----------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n见正文各节。\n\n### 6. 技术架构\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n见正文使用场景。\n\n### 9. 竞品对比\n见正文对比(如有)。\n\n### 10. 配置参考\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "496869c9e4284758614336b69ec0353ec8099ca9": {"t": 1785754110.4240215, "n": 49510, "h": "588578e2ba18413a", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\n num_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ---- | ---- | ---- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{ \"llm\": { \"pricing\": {\n \"openai\": [ { \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 } ],\n \"myprovider\": [ { \"match\": \"custom\", \"in\": 2, \"out\": 8 } ]\n} } }\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答\n+ 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条\nagent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ``` 代码围栏里也能提取(取首个配平 `{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\n compressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n|---|---|\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config`\n+ 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n```jsonc\n{ \"ide\":\"vscode\", \"transport\":\"sse\", \"url\":\"http://127.0.0.1:53690/sse\",\n \"port\":53690, \"workspaceFolders\":[\"/abs/ws\"], \"token\":\"\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件/终端输出**:每次提交 prompt(headless 单轮、stream 每轮、\n REPL 每条),CLI 自动调一次 `getSelection` + `getOpenEditors`(+ 连了终端工具时 `getTerminalOutput`),\n 把活跃文件、打开的 tab、当前选中代码(截断到 2000 字符)、**最近 2 条终端命令 + 输出**\n (每条输出截断到 800 字符、保留末尾)作为 `` 块附进**本轮用户消息**——模型无须\n 自己决定调工具(对标 Claude Code「把终端输出带进 prompt」)。该块是**短暂的**:只进在途消息,\n 不写入会话持久化(resume 回放的是你的原话,不是过期的编辑器快照)。IDE 无响应时 1.5s 超时\n 放弃,绝不阻塞回合。**终端输出可单独关**:`CC_IDE_TERMINAL=0` 只去掉终端(选区/编辑器仍注入),\n 适合终端噪声大或含敏感信息时。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` at-mention(显式版)**:除了上面\"每轮自动共享\"的隐式注入,\n 你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics` 把\n **整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件)。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会误匹配),\n 在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,不写\n 持久化)。未连 IDE / 无选区 / 无问题时,`@mention` 原样留在文中并打印一条 `@ide:` 提示。\n 与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\n user requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\" // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n }\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\n resource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ``` 自动升级围栏)+ compact 标记 + token 汇总。\n `last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n|---|---|\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ---- | ----------- | ------------------ | -- |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n { \"matcher\": \"Bash\",\n \"hooks\": [ { \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 } ] }\n ],\n \"PostToolUse\": [\n { \"matcher\": \"Edit|Write\", \"hooks\": [ { \"type\": \"command\", \"command\": \"./fmt.sh\" } ] }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| --------- | ---- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON)| continue(放行;stdout 可作 `additionalContext`)|\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent)|\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| ---- | ------ | ---- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| ----------------------- | ------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| ------------ | ----------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n见正文各节。\n\n### 6. 技术架构\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n见正文使用场景。\n\n### 9. 竞品对比\n见正文对比(如有)。\n\n### 10. 配置参考\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "6356e4a58c0c82c7326dd2be402021445ac473ce": {"t": 1785754111.892107, "n": 49327, "h": "6842dbaba7da9b94", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\n num_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ---- | ---- | ---- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{ \"llm\": { \"pricing\": {\n \"openai\": [ { \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 } ],\n \"myprovider\": [ { \"match\": \"custom\", \"in\": 2, \"out\": 8 } ]\n} } }\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答\n+ 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条\nagent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ``` 代码围栏里也能提取(取首个配平 `{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\n compressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n|---|---|\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config`\n+ 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n```jsonc\n{ \"ide\":\"vscode\", \"transport\":\"sse\", \"url\":\"http://127.0.0.1:53690/sse\",\n \"port\":53690, \"workspaceFolders\":[\"/abs/ws\"], \"token\":\"\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件**:每次提交 prompt(headless 单轮、stream 每轮、REPL 每条),\n CLI 自动调一次 `getSelection` + `getOpenEditors`,把活跃文件、打开的 tab、当前选中代码\n (截断到 2000 字符)作为 `` 块附进**本轮用户消息**——模型无须自己决定调工具。\n 该块是**短暂的**:只进在途消息,不写入会话持久化(resume 回放的是你的原话,不是过期的\n 编辑器快照)。IDE 无响应时 1.5s 超时放弃,绝不阻塞回合。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` at-mention(显式版)**:除了上面\"每轮自动共享\"的隐式注入,\n 你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics` 把\n **整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件)。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会误匹配),\n 在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,不写\n 持久化)。未连 IDE / 无选区 / 无问题时,`@mention` 原样留在文中并打印一条 `@ide:` 提示。\n 与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n- **diff 行内批注(Request changes…)**:除 Accept/Reject/Pick hunks,diff 还有第三个选择\n **「Request changes…」**——在右侧改动 pane 选中要评论的行,逐条写修改意见(锚定所选行,\n 空输入结束评审)。文件**不落盘**,这些行锚批注作为工具结果回喂 agent(`[IDE review] … the\n user requested changes: • line N: <意见> ⟪锚文本⟫ … propose it again`),agent 据此修订后\n 重新提案——对标 Claude Code 的 inline-review。零意见的评审降级为 Reject(不写文件,fail-safe)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\" // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n }\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\n resource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ``` 自动升级围栏)+ compact 标记 + token 汇总。\n `last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **交互 REPL `/tasks`**(对照 Claude Code 后台任务面板) — 列出 agent 用\n `run_shell { run_in_background:true }` 起的**后台 shell 任务**(否则只有 agent 经 `check_shell`\n 工具看得见):状态徽章(`● running`/`✓ exited (0)`/`✗ failed (code)`/`✗ error`)+ 运行时长 +\n 命令。管理子命令:`/tasks kill ` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n|---|---|\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ---- | ----------- | ------------------ | -- |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n { \"matcher\": \"Bash\",\n \"hooks\": [ { \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 } ] }\n ],\n \"PostToolUse\": [\n { \"matcher\": \"Edit|Write\", \"hooks\": [ { \"type\": \"command\", \"command\": \"./fmt.sh\" } ] }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| --------- | ---- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON)| continue(放行;stdout 可作 `additionalContext`)|\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent)|\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| ---- | ------ | ---- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| ----------------------- | ------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| ------------ | ----------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n见正文各节。\n\n### 6. 技术架构\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n见正文使用场景。\n\n### 9. 竞品对比\n见正文对比(如有)。\n\n### 10. 配置参考\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "e38fe9611940d9533ec0953179b95fc41d6aab91": {"t": 1785754113.434194, "n": 48628, "h": "4a27cf4d30879964", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\n num_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ---- | ---- | ---- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n### 备用模型链(`--fallback-model`)\n\n无人值守运行时,当主模型调用失败就按顺序切到备用模型重试(Claude-Code 2.1.166/2.1.152 平价):\n\n```bash\n# 可重复传,或逗号分隔;最多 3 个,按序尝试\nchainlesschain agent -p \"...\" --fallback-model backup-a --fallback-model backup-b\nchainlesschain agent -p \"...\" --fallback-model \"backup-a,backup-b,backup-c\"\n```\n\n- **触发条件**:① 瞬时错误(overload / rate-limit / 5xx / 网络抖动 `isRetryableModelError`);② **主模型 not-found**(404 / \"model not found\" / \"unknown model\" / \"does not exist\" `isModelNotFoundError`)——配置的模型 id 被下线或拼错时也能自动切。普通 4xx(认证 / 配额 / 坏请求)**不**重试,直接抛出。\n- **同 provider**:每跳只换 `model`,保留 provider / baseUrl / apiKey(跨 provider fallback 是更大的特性,未做)。\n- **链式**:主 → 备1 → 备2 → 备3,逐个尝试直到成功或链耗尽(抛最后一个错误)。与刚试过的模型相同的备用项会被跳过(不浪费一次往返)。\n- **默认链**:不传 flag 时读 `config.llm.fallbackModels`(数组或逗号串)/ 旧式 `config.llm.fallbackModel`(单个),无人值守运行无需带 flag。flag 永远优先于 config。\n- **交互 REPL 也生效**:会话内 LLM 调用失败时同样按链重试,黄字提示每一跳。\n- 实现:纯模块 `src/runtime/fallback-model.js`(挂在 agentLoop 的 `options.chatFn` seam 上,runner 零改动);提示走 stderr 保持 stdout 干净。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{ \"llm\": { \"pricing\": {\n \"openai\": [ { \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 } ],\n \"myprovider\": [ { \"match\": \"custom\", \"in\": 2, \"out\": 8 } ]\n} } }\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答\n+ 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条\nagent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ``` 代码围栏里也能提取(取首个配平 `{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\n compressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n|---|---|\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config`\n+ 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n```jsonc\n{ \"ide\":\"vscode\", \"transport\":\"sse\", \"url\":\"http://127.0.0.1:53690/sse\",\n \"port\":53690, \"workspaceFolders\":[\"/abs/ws\"], \"token\":\"\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件**:每次提交 prompt(headless 单轮、stream 每轮、REPL 每条),\n CLI 自动调一次 `getSelection` + `getOpenEditors`,把活跃文件、打开的 tab、当前选中代码\n (截断到 2000 字符)作为 `` 块附进**本轮用户消息**——模型无须自己决定调工具。\n 该块是**短暂的**:只进在途消息,不写入会话持久化(resume 回放的是你的原话,不是过期的\n 编辑器快照)。IDE 无响应时 1.5s 超时放弃,绝不阻塞回合。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` at-mention(显式版)**:除了上面\"每轮自动共享\"的隐式注入,\n 你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics` 把\n **整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件)。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会误匹配),\n 在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,不写\n 持久化)。未连 IDE / 无选区 / 无问题时,`@mention` 原样留在文中并打印一条 `@ide:` 提示。\n 与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\" // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n }\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\n resource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ``` 自动升级围栏)+ compact 标记 + token 汇总。\n `last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n|---|---|\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ---- | ----------- | ------------------ | -- |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n { \"matcher\": \"Bash\",\n \"hooks\": [ { \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 } ] }\n ],\n \"PostToolUse\": [\n { \"matcher\": \"Edit|Write\", \"hooks\": [ { \"type\": \"command\", \"command\": \"./fmt.sh\" } ] }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| --------- | ---- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON)| continue(放行;stdout 可作 `additionalContext`)|\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent)|\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| ---- | ------ | ---- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| ----------------------- | ------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| ------------ | ----------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n\n## 附录:规范章节补全(v5.0.3.108)\n\n> 为对齐项目用户文档标准结构,下列章节以 `见正文` 指引或简述方式补齐若干视角,不重复正文细节。\n\n### 1. 概述\n见正文头部。CLI Managed Agents & Hosted Session API:托管 agent + headless 命令。\n\n### 2. 核心特性\ncc agent headless / cost / checkpoint / loop / hooks / 权限。\n\n### 3. 系统架构\n见正文 / [系统架构](../design/系统设计_主文档.md)(三端 + 双后端 + P2P)。\n\n### 4. 系统定位\nChainlessChain 的「CLI 托管 Agent」。\n\n### 5. 核心功能\n见正文各节。\n\n### 6. 技术架构\nElectron + Vue3 / Spring Boot + FastAPI / libp2p + Signal / SQLCipher(按需)。\n\n### 7. 系统特点\n见正文(步骤 / 版本 / 注意事项)。\n\n### 8. 应用场景\n见正文使用场景。\n\n### 9. 竞品对比\n见正文对比(如有)。\n\n### 10. 配置参考\n见正文配置 / 环境变量章节;`.chainlesschain/config.json`。\n\n### 11. 性能指标\n见正文性能 / 资源要求(如有)。\n\n### 12. 测试覆盖\n见正文验证步骤(如有)。\n\n### 13. 安全考虑\n见正文安全 / 密钥章节;本地加密 + U盾/SIMKey(如适用)。\n\n### 14. 故障排除\n见正文故障排查 / 常见问题章节。\n\n### 15. 关键文件\n见正文涉及的文件 / 目录。\n\n### 16. 使用示例\n见正文命令 / 操作示例。\n\n### 17. 相关文档\n[快速开始](./QUICK_START.md)、[安装指南](./INSTALLATION.md)、其它用户文档。\n"}, "dc67bb228ade50552c4c466ae7c2d65f6f9b300b": {"t": 1785754115.246544, "n": 46776, "h": "4b64bdb38f6bec89", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\n num_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ---- | ---- | ---- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{ \"llm\": { \"pricing\": {\n \"openai\": [ { \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 } ],\n \"myprovider\": [ { \"match\": \"custom\", \"in\": 2, \"out\": 8 } ]\n} } }\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答\n+ 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条\nagent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ``` 代码围栏里也能提取(取首个配平 `{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\n compressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n|---|---|\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config`\n+ 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n```jsonc\n{ \"ide\":\"vscode\", \"transport\":\"sse\", \"url\":\"http://127.0.0.1:53690/sse\",\n \"port\":53690, \"workspaceFolders\":[\"/abs/ws\"], \"token\":\"\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件**:每次提交 prompt(headless 单轮、stream 每轮、REPL 每条),\n CLI 自动调一次 `getSelection` + `getOpenEditors`,把活跃文件、打开的 tab、当前选中代码\n (截断到 2000 字符)作为 `` 块附进**本轮用户消息**——模型无须自己决定调工具。\n 该块是**短暂的**:只进在途消息,不写入会话持久化(resume 回放的是你的原话,不是过期的\n 编辑器快照)。IDE 无响应时 1.5s 超时放弃,绝不阻塞回合。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` at-mention(显式版)**:除了上面\"每轮自动共享\"的隐式注入,\n 你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics` 把\n **整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件)。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会误匹配),\n 在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,不写\n 持久化)。未连 IDE / 无选区 / 无问题时,`@mention` 原样留在文中并打印一条 `@ide:` 提示。\n 与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\" // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n }\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\n resource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ``` 自动升级围栏)+ compact 标记 + token 汇总。\n `last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **交互 REPL `/sessions`**(Claude-Code `/resume` 平价的发现半边) — 列出**可恢复的历史会话**\n (跨 DB + JSONL 两库, 经 `listRecentSessions`):短 id / 来源 / 消息数 / 时间 / 标题, 标注当前会话。\n 配合既有 `/session resume `(会话内原地切换:重建消息+换 sessionId)= 完整 resume 体验。\n `/session`(单数)= 当前会话信息;`/sessions`(复数)= 列表。\n- **交互 REPL `/memory`**(Claude-Code 平价) — 列出 agent **自动载入系统提示**的项目记忆文件\n (cc.md > CLAUDE.md > AGENTS.md 层级 + @import + 命中的 path-scoped rules),每个显\n scope/路径/字节 + 总计。`CC_PROJECT_MEMORY=0` 时标注\"已禁用\"。等价 `cc memory files` 的\n 会话内即时版;区别于 `#`(往 cc.md 追加笔记)与 `cc memory recall`(scoped 记忆库)。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n|---|---|\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ---- | ----------- | ------------------ | -- |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n { \"matcher\": \"Bash\",\n \"hooks\": [ { \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 } ] }\n ],\n \"PostToolUse\": [\n { \"matcher\": \"Edit|Write\", \"hooks\": [ { \"type\": \"command\", \"command\": \"./fmt.sh\" } ] }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| --------- | ---- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON)| continue(放行;stdout 可作 `additionalContext`)|\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent)|\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| ---- | ------ | ---- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| ----------------------- | ------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| ------------ | ----------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n"}, "8b42b8c1896667597711ebfd9c8d25833ef4c6af": {"t": 1785754117.189065, "n": 46223, "h": "2daa72ce6fcc514b", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\n num_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ---- | ---- | ---- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{ \"llm\": { \"pricing\": {\n \"openai\": [ { \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 } ],\n \"myprovider\": [ { \"match\": \"custom\", \"in\": 2, \"out\": 8 } ]\n} } }\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答\n+ 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条\nagent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ``` 代码围栏里也能提取(取首个配平 `{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\n compressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n|---|---|\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config`\n+ 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n```jsonc\n{ \"ide\":\"vscode\", \"transport\":\"sse\", \"url\":\"http://127.0.0.1:53690/sse\",\n \"port\":53690, \"workspaceFolders\":[\"/abs/ws\"], \"token\":\"\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件**:每次提交 prompt(headless 单轮、stream 每轮、REPL 每条),\n CLI 自动调一次 `getSelection` + `getOpenEditors`,把活跃文件、打开的 tab、当前选中代码\n (截断到 2000 字符)作为 `` 块附进**本轮用户消息**——模型无须自己决定调工具。\n 该块是**短暂的**:只进在途消息,不写入会话持久化(resume 回放的是你的原话,不是过期的\n 编辑器快照)。IDE 无响应时 1.5s 超时放弃,绝不阻塞回合。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` at-mention(显式版)**:除了上面\"每轮自动共享\"的隐式注入,\n 你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics` 把\n **整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件)。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会误匹配),\n 在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,不写\n 持久化)。未连 IDE / 无选区 / 无问题时,`@mention` 原样留在文中并打印一条 `@ide:` 提示。\n 与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\" // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n }\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\n resource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ``` 自动升级围栏)+ compact 标记 + token 汇总。\n `last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **交互 REPL `/doctor`**(Claude-Code 平价) — 一屏**会话健康检查**:把 LLM provider/model、\n API key 是否设置、IDE 桥接、MCP 服务器、权限规则数、settings.json hooks 汇成一个\n ✓/⚠/· 列表,并标记最常见的\"聊天没反应\"成因(未配 provider、云 provider 缺 key)。\n 比逐个 `/config`·`/ide`·`/mcp`·`/permissions` 更快定位 setup 问题。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n|---|---|\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ---- | ----------- | ------------------ | -- |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n { \"matcher\": \"Bash\",\n \"hooks\": [ { \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 } ] }\n ],\n \"PostToolUse\": [\n { \"matcher\": \"Edit|Write\", \"hooks\": [ { \"type\": \"command\", \"command\": \"./fmt.sh\" } ] }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| --------- | ---- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON)| continue(放行;stdout 可作 `additionalContext`)|\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent)|\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| ---- | ------ | ---- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| ----------------------- | ------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| ------------ | ----------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n"}, "70a7770e1a3701743b158ede86ea6af1e188883d": {"t": 1785754118.8339415, "n": 45966, "h": "32db1f87f7da248e", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\n num_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ---- | ---- | ---- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{ \"llm\": { \"pricing\": {\n \"openai\": [ { \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 } ],\n \"myprovider\": [ { \"match\": \"custom\", \"in\": 2, \"out\": 8 } ]\n} } }\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答\n+ 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条\nagent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ``` 代码围栏里也能提取(取首个配平 `{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\n compressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n|---|---|\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config`\n+ 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n```jsonc\n{ \"ide\":\"vscode\", \"transport\":\"sse\", \"url\":\"http://127.0.0.1:53690/sse\",\n \"port\":53690, \"workspaceFolders\":[\"/abs/ws\"], \"token\":\"\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件**:每次提交 prompt(headless 单轮、stream 每轮、REPL 每条),\n CLI 自动调一次 `getSelection` + `getOpenEditors`,把活跃文件、打开的 tab、当前选中代码\n (截断到 2000 字符)作为 `` 块附进**本轮用户消息**——模型无须自己决定调工具。\n 该块是**短暂的**:只进在途消息,不写入会话持久化(resume 回放的是你的原话,不是过期的\n 编辑器快照)。IDE 无响应时 1.5s 超时放弃,绝不阻塞回合。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` at-mention(显式版)**:除了上面\"每轮自动共享\"的隐式注入,\n 你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics` 把\n **整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件)。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会误匹配),\n 在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,不写\n 持久化)。未连 IDE / 无选区 / 无问题时,`@mention` 原样留在文中并打印一条 `@ide:` 提示。\n 与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\" // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n }\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\n resource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ``` 自动升级围栏)+ compact 标记 + token 汇总。\n `last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **交互 REPL `/config`**(Claude-Code 平价) — 显示**生效配置**(密钥安全):当前 LLM\n provider/model/baseUrl、key 是否设置(只显 `set (…后4位)`/`not set`,绝不打印明文)、\n web-search 后端、config 文件路径,以及本会话**实际生效**的 provider/model(经 `--provider`/\n settings 覆盖时标 `(overrides config)`)。读 `~/.chainlesschain/config.json`。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n|---|---|\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ---- | ----------- | ------------------ | -- |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n { \"matcher\": \"Bash\",\n \"hooks\": [ { \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 } ] }\n ],\n \"PostToolUse\": [\n { \"matcher\": \"Edit|Write\", \"hooks\": [ { \"type\": \"command\", \"command\": \"./fmt.sh\" } ] }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| --------- | ---- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON)| continue(放行;stdout 可作 `additionalContext`)|\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent)|\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| ---- | ------ | ---- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| ----------------------- | ------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| ------------ | ----------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n"}, "cfba153e48d2ca364dec899a96cc7fbc25da1220": {"t": 1785754120.6492138, "n": 45684, "h": "97d6d0c45b06b4a0", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\n num_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ---- | ---- | ---- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{ \"llm\": { \"pricing\": {\n \"openai\": [ { \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 } ],\n \"myprovider\": [ { \"match\": \"custom\", \"in\": 2, \"out\": 8 } ]\n} } }\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答\n+ 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条\nagent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ``` 代码围栏里也能提取(取首个配平 `{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\n compressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n|---|---|\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config`\n+ 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n```jsonc\n{ \"ide\":\"vscode\", \"transport\":\"sse\", \"url\":\"http://127.0.0.1:53690/sse\",\n \"port\":53690, \"workspaceFolders\":[\"/abs/ws\"], \"token\":\"\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件**:每次提交 prompt(headless 单轮、stream 每轮、REPL 每条),\n CLI 自动调一次 `getSelection` + `getOpenEditors`,把活跃文件、打开的 tab、当前选中代码\n (截断到 2000 字符)作为 `` 块附进**本轮用户消息**——模型无须自己决定调工具。\n 该块是**短暂的**:只进在途消息,不写入会话持久化(resume 回放的是你的原话,不是过期的\n 编辑器快照)。IDE 无响应时 1.5s 超时放弃,绝不阻塞回合。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` at-mention(显式版)**:除了上面\"每轮自动共享\"的隐式注入,\n 你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics` 把\n **整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件)。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会误匹配),\n 在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,不写\n 持久化)。未连 IDE / 无选区 / 无问题时,`@mention` 原样留在文中并打印一条 `@ide:` 提示。\n 与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\" // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n }\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\n resource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ``` 自动升级围栏)+ compact 标记 + token 汇总。\n `last` 直接导最近一次 agent 会话(不碰 DB)。\n- **交互 REPL `/export [path]`**(Claude-Code 平价) — 把**当前内存里的对话**导出为 Markdown\n (user/assistant/system 轮次 + tool_call 的 JSON 参数美化 + tool_result 围栏块 4K 截断 +\n 多模态图片标 `[image]`)。区别于 `cc session export`(读持久化 JSONL)与 `cc export`(知识库导出):\n `/export` 抓的是此刻 context 里的内容,持久化与否都能导。省略路径时落\n `./chainlesschain-export-<时间戳>.md`。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n|---|---|\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ---- | ----------- | ------------------ | -- |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n { \"matcher\": \"Bash\",\n \"hooks\": [ { \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 } ] }\n ],\n \"PostToolUse\": [\n { \"matcher\": \"Edit|Write\", \"hooks\": [ { \"type\": \"command\", \"command\": \"./fmt.sh\" } ] }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| --------- | ---- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON)| continue(放行;stdout 可作 `additionalContext`)|\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent)|\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| ---- | ------ | ---- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| ----------------------- | ------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| ------------ | ----------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n"}, "6e51b342b1a741a07d2442f30f987f4a5e7b65aa": {"t": 1785754122.3204453, "n": 45369, "h": "6fcf3238bfe8a4bb", "c": "# CLI — Managed Agents & Hosted Session API\n\n> Parent: [`../CLI_COMMANDS_REFERENCE.md`](../CLI_COMMANDS_REFERENCE.md)\n\n## Managed Agents Parity (session-core)\n\nPhase D–H — shared with Desktop via `@chainlesschain/session-core`.\nAll persist under `~/.chainlesschain/`; Desktop reads the same files from `/.chainlesschain/`.\n\n```bash\n# 作用域记忆 (MemoryStore — memory-store.json)\nchainlesschain memory store \"Prefers TypeScript\" --scope global --category preference\nchainlesschain memory store \"Asked about P2P\" --scope session --scope-id sess_123 --tags p2p,q\nchainlesschain memory store \"Likes Rust\" --scope user --scope-id u_alice --category preference\nchainlesschain memory recall \"typescript\" --scope global --json\nchainlesschain memory recall \"rust\" --scope user --scope-id u_alice --json\nchainlesschain memory recall --tags p2p --limit 20\n\n# 将 JSONL 会话轨迹整合进 MemoryStore (Phase G)\nchainlesschain memory consolidate --session sess_123 --scope agent --agent-id coder\nchainlesschain memory consolidate --session sess_123 --dry-run --json\n\n# 按会话的审批策略 (ApprovalGate — approval-policies.json)\nchainlesschain session policy sess_123 # 显示当前策略\nchainlesschain session policy sess_123 --set trusted # strict | trusted | autopilot\nchainlesschain session policy sess_123 --json\n\n# Beta 功能开关 (BetaFlags — beta-flags.json)\n# 开关格式: -\nchainlesschain config beta list [--json]\nchainlesschain config beta enable idle-park-2026-05-01\nchainlesschain config beta disable idle-park-2026-05-01\n\n# 会话生命周期 (SessionManager — parked-sessions.json) · Phase H\nchainlesschain session lifecycle # 列出运行中和已驻留的会话\nchainlesschain session lifecycle --status parked --json\nchainlesschain session park sess_123 # 标记空闲并驻留\nchainlesschain session unpark sess_123 # 恢复已驻留会话\nchainlesschain session end sess_123 --consolidate # 关闭会话并写入轨迹到 MemoryStore\nchainlesschain agent --session sess_123 # 启动时自动恢复驻留会话\nchainlesschain agent --no-park-on-exit # 退出时关闭句柄而非驻留\n\n# 可脚本化的 StreamRouter (stdout 输出 NDJSON) · Phase H\nchainlesschain stream \"summarize file X\" # 每行一个 {type,ts,...}\nchainlesschain stream \"...\" --text # collect() 拼接后的文本\nchainlesschain stream \"...\" --provider openai --model gpt-4o\n\n# Phase I — 会话 tail + 用量汇总\nchainlesschain session tail sess_123 # 跟随实时 JSONL 事件 (NDJSON)\nchainlesschain session tail sess_123 --from-start # 从字节 0 重放\nchainlesschain session tail sess_123 --type tool_call,assistant_message\nchainlesschain session tail sess_123 --since 1712000000000 --once\nchainlesschain session usage sess_123 # 单会话 token 汇总\nchainlesschain session usage # 全局汇总\nchainlesschain session usage --json --limit 500\n```\n\n## Headless / Print Mode (`agent -p`)\n\nClaude Code `claude -p` 对标:`cc agent` 的**单轮非交互**执行——进 CI / 管道 /\n脚本。绕过交互 REPL,prompt 来自 `-p` 值 / 位置参数 / stdin 管道,跑完即退出\n(agent 内部仍可多轮工具循环)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"review @src/x.js\" # 支持 @path 文件引用\necho \"task\" | chainlesschain agent -p # 从 stdin 读取任务\nchainlesschain agent -p \"...\" --output-format json # text(默认) | json | stream-json\nchainlesschain agent -p \"...\" --max-turns 5 # 限制 agent 循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools \"read_file,git\" # 工具白名单\nchainlesschain agent -p \"...\" --disallowed-tools \"run_shell\" # 工具黑名单\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # 见下表\nchainlesschain agent -p \"...\" --add-dir ../lib # 额外工作根 (可重复)\nchainlesschain agent -p \"...\" --goal # 绑定 cc goal,每轮对照推进 (见 Goal 节)\nchainlesschain agent -p \"...\" --goal --goal-assess # 自动选活跃目标 + 跑完自评进度\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量 (见下)\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时挂载 MCP server (见 MCP 节)\nchainlesschain agent -p \"...\" --no-mcp # 不自动连 cc mcp add --auto-connect 的服务\nchainlesschain agent -p \"...\" --mcp-config ./m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 settings:权限 + model/env 覆盖 (见 Permissions)\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --think # Anthropic 扩展思考 (见下「扩展思考」)\nchainlesschain agent -p \"...\" --ultrathink # = --think ultra (最高强度)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"这张图里是什么?\" # 多模态 / 图像输入 (可重复 --image,见下)\n```\n\n**输出格式**(`--output-format`):\n- `text`(默认)——最终回答 → stdout;工具轨迹 → stderr(保证管道纯净)。\n- `json`——单个结果信封 `{ type:\"result\", subtype, is_error, result, session_id,\n num_turns, duration_ms, tool_calls[], usage }`。\n- `stream-json`——逐事件 NDJSON:首行 `{type:\"system\",subtype:\"init\",...}`,\n 随后 `tool_use` / `tool_result` / `token_usage`,末行 `{type:\"result\",...}`。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`):\n在常规事件之外额外 emit `{type:\"stream_event\",event:{type:\"content_block_delta\",\ndelta:{type:\"text_delta\",text}}}`——assistant 文本实时流出,适合转发 UI。真增量目前\n对 **ollama**(本地默认)+ **anthropic / OpenAI 兼容**(openai/deepseek/dashscope/\nmistral/gemini/volcengine)全部生效(含流式 tool-call 重组);其它 provider 静默退回整段。\n\n**权限模式**(`--permission-mode`,映射 ApprovalGate 层级;headless 无法弹窗,\n故默认 **fail-closed**):\n\n| mode | 层级 | 行为 |\n| ---- | ---- | ---- |\n| (默认) | STRICT | 拒绝中/高危 shell(confirm 自动否决);文件编辑放行 |\n| `plan` | STRICT | 同上,且**工具收窄为只读集**(read_file/search_files/list_dir/...) |\n| `acceptEdits` | TRUSTED | 高危 shell 仍拒,中危放行 |\n| `bypassPermissions` | AUTOPILOT | 全放行,不确认 |\n\n退出码:成功 `0`;`--max-turns` 耗尽(`error_max_turns`)或错误 `1`。\n会话恢复:`--resume ` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session ]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume --fork-session # 不污染原会话,开新分支\n```\n\n### 扩展思考(`--think` / `--ultrathink` / `--thinking-budget`)\n\nAnthropic extended thinking 的 opt-in 开关(其它 provider 自动忽略;o-series /\nDeepSeek-reasoner 由模型自身决定)。强度从 `--think` 的可选值取:\n\n- `--think`(裸用)→ 默认强度;`--think hard` / `--think ultra` 指定强度;`--ultrathink` = `--think ultra`。\n- **自适应思考模型**(Opus 4.6/4.7/4.8、Sonnet 4.6)→ 走 `output_config.effort`(`--think` 的强度映射成 effort)。\n- **legacy 思考模型**(Sonnet 4.5、Opus 4.0–4.5 及更早)→ 走 `enabled + budget_tokens`;`--thinking-budget ` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image `)\n\n给视觉模型附图(headless;可重复 `--image` 传多张):\n\n```bash\nchainlesschain agent --image a.png --image b.jpg -p \"对比这两张图\" --provider volcengine --model doubao-seed-1-6-251015\n```\n\n- 支持扩展名:`png` / `jpg` / `jpeg` / `gif` / `webp`;不支持的扩展名会**立刻报错**而非发出坏请求。\n- 内部统一成 OpenAI 形状的多模态消息(`image_url` data-URL),再按 provider 转换:\n **OpenAI 兼容**(volcengine/doubao、openai、…)原样透传;**ollama** 转成 `{content, images:[base64]}`;\n **anthropic** 转成 `image` content block(base64 source)。纯文本运行的请求形状**字节不变**。\n- 仅 headless 路径生效(`-p` / 位置参数 / stdin 管道);交互会话传 `--image` 会被忽略并给出提示。\n\n## Cost — 估算 $ 花费\n\n在 `session usage`(token 计数)之上叠加价格层(`src/lib/llm-pricing.js`)。读取\n同一份 JSONL `token_usage` 事件,所以无新数据采集——纯报表视图。\n\n```bash\nchainlesschain cost # 全局花费汇总 (按 provider/model 拆分)\nchainlesschain cost # 单会话花费\nchainlesschain cost --json # 机器可读\nchainlesschain cost --limit 500 # 全局汇总最多扫描的会话数\n```\n\n**交互 REPL `/cost`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/cost` 看**本会话至今**的 token 花费 + 估算 $(按 provider/model 拆分)。与 `cc cost` 复用同一 `llm-pricing` 价格表 + `config.llm.pricing` 覆盖,但**内存累计**每轮 usage——不依赖会话持久化,匿名会话也能用。\n\n规则:\n- 本地 provider(`ollama` / `local` / `llamacpp` / `mediapipe`)= **免费**(matched + free)。\n- 未知模型 → **`unpriced`**(不臆测),其 token **排除**出 `cost.totalCost` 并在 `unpriced[]` 单列。\n- 价格是公开 list 价的**估算**(USD / 1M tokens),内置覆盖 anthropic / openai /\n deepseek / volcengine(doubao)。\n\n`--json` 输出形状(在 `session usage` 聚合上加 cost 字段):\n\n```jsonc\n{\n \"total\": { \"inputTokens\", \"outputTokens\", \"totalTokens\", \"calls\" },\n \"byModel\": [ { \"provider\", \"model\", \"inputTokens\", \"outputTokens\",\n \"cost\", \"currency\": \"USD\", \"matched\", \"free\" } ],\n \"cost\": { \"totalCost\", \"currency\": \"USD\", \"unpricedCount\" },\n \"unpriced\": [ { \"provider\", \"model\", \"totalTokens\" } ]\n}\n```\n\n**配置价格覆盖**(无需改源码)——`config.llm.pricing`,同名 `match` 替换内置、\n新 provider 追加、畸形条目跳过:\n\n```jsonc\n// .chainlesschain/config.json\n{ \"llm\": { \"pricing\": {\n \"openai\": [ { \"match\": \"gpt-4o\", \"in\": 2.5, \"out\": 10 } ],\n \"myprovider\": [ { \"match\": \"custom\", \"in\": 2, \"out\": 8 } ]\n} } }\n```\n\n## Checkpoint — 文件状态快照 / 回滚\n\nClaude Code \"rewind\" 的 CLI 对标:有风险的 agent 运行前给文件拍快照,出问题再还原。\n**区别于 `cc workflow checkpoint`**(后者是工作流**执行态**,存 DB,不是文件)。\n\n**双引擎**(按 cwd 自动选):\n- **git-plumbing(默认,git 仓内)** `src/lib/checkpoint-store.js` — 整工作树快照,\n 经**临时 index**(`GIT_INDEX_FILE`)写成 `refs/cc-checkpoints//` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints//`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 \nchainlesschain checkpoint create [paths...] [--label ] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show # 清单(文件 + 字节数)\nchainlesschain checkpoint show --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir

`(目标目录,默认 `.`)、`-s, --session `(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore `)。\n- git 引擎按 `-s/--session` 分组;copy 引擎不分 session。\n- copy 引擎跳过 `node_modules`/`.git`/`dist`/`build`/`.chainlesschain` 等重目录,\n `maxFiles` 默认 2000 防误快照整盘。\n\n### 自动快照(agent 工具循环)\n\n**git 仓内默认开启**(Claude-Code 同款默认;shadow-commit 引擎零触工作区/真索引):\n在**每个会改文件的工具调用前**自动快照工作树,出问题用 `cc checkpoint restore `\n回滚到该工具调用之前。`--no-checkpoint` 关闭;非 git 目录默认关(copy 引擎写真实文件,\n不做静默默认),显式 `--checkpoint` 可强制开:\n\n```bash\ncc agent -p \"重构 auth 模块\" # git 仓内默认就有自动快照\ncc agent --no-checkpoint -p \"...\" # 本次关闭\ncc agent --checkpoint -p \"...\" # 非 git 目录强制开(copy 引擎)\n# 每个 write_file/edit_file/run_shell/run_code 前打印 ⎌ checkpoint cpNNNN (before )\ncc checkpoint list\ncc checkpoint restore cp0003 # 回到第 3 个工具调用之前(先自动拍 safety)\n```\n\n- 只读工具(`read_file`/`search_files`/`list_dir`/`list_skills`/`search_sessions`)跳过。\n- 快照落在 `refs/cc-checkpoints//`;连续工具未改文件时去重(不堆冗余 ref)。\n- best-effort:快照失败绝不阻塞工具;非 git 仓静默 no-op。\n- stream-json 输出对应事件:`{ \"type\":\"checkpoint\", \"id\":\"cpNNNN\", \"tool\":\"write_file\" }`。\n\n## Goal — 跨会话持久目标 / OKR\n\n`cc goal` 是**超出 Claude Code 的扩展**(Claude Code 无对应):一个长期目标,agent\n跨多个会话朝它推进,每轮把目标注入到 agent 循环里对照。**区别于**:`cc session`\n(短期上下文)、`cc memory`(事实)、`cc planmode`(单次运行的计划)、`cc workflow`\n(执行态)。落盘在 `~/.chainlesschain/goals/.json`(home 目录 → 天然跨会话)。\n\n分三个 Phase 落地(`src/lib/goal-store.js` / `goal-context.js` / `goal-assess.js`):\n\n### Phase 0 — 存储 + 命令\n\n```bash\nchainlesschain goal set \"上线 v2 API\" --kr \"写完测试\" --kr \"迁移文档\" # 建目标 (+关键结果)\nchainlesschain goal list [--status active] [--json] # 列出 (含 >14 天无进展的 ⚠ stale 提示)\nchainlesschain goal show # 详情\nchainlesschain goal kr add \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set --current 5 # current≥target 自动标 done\nchainlesschain goal progress --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon # 生命周期\nchainlesschain goal active [--session ] # 看哪个目标会绑定到运行\nchainlesschain goal rm [--force] # 删除\n```\n\n进度自动从「已完成 KR / 总 KR」推导;手动 `--pct` 可覆盖。`done` 时进度补到 100%。\n\n### Phase 1 — 注入 headless 运行(`--goal`)\n\n`cc agent` 把解析到的活跃目标注入到 agent 循环,**每轮**追加一段简短的目标提醒\n(目标 + 未完成 KR + 进度%),不污染会话历史(经 `prepareCall` 临时 system 补充,\n**与默认 turn-context 合成而非替换**)。\n\n```bash\nchainlesschain agent -p \"继续推进\" --goal # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal `(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal ` 还会把会话 link 到目标,使后续 `--continue`/`--resume` 仍带该目标。\n`stream-json` 的 init 信封含 `goal_id`。best-effort:绑定失败绝不影响运行。\n\n### Phase 2 — 跑完自评进度(`--goal-assess`,opt-in)\n\n`--goal-assess` 在一次 `--goal` 运行结束后,**额外**让模型从运行轨迹(任务 + 最终回答\n+ 用到的工具)判断目标是否推进,并把结果落盘:进度、关键结果 current/done、一条\nagent 署名的进度 note、以及 drift 标记。**因为多花一次 completion 所以默认关闭**。\n\n```bash\nchainlesschain agent -p \"实现登录接口\" --goal --goal-assess\n# 跑完后:◎ goal : advanced (45%) ← 自动写回目标\nchainlesschain goal show # 能看到 agent 署名的进度 note + KR 更新\n```\n\n- 解析宽容:模型把 JSON 裹在散文 / ``` 代码围栏里也能提取(取首个配平 `{...}`)。\n- 每个子更新独立 try/catch:坏的 KR id 不会连累进度 note 落盘。\n- drift:本次运行未推进 → 记 `no-progress` 标记;模型给的 concerns 一并记入。\n- `stream-json` 输出 `{ \"type\":\"goal_assessment\", \"goal_id\", \"advanced\", \"progress\", \"note\" }`。\n- best-effort:自评失败绝不改变运行本身的退出码 / 结果。\n\n## Compaction — 上下文自动压缩 + `cc compact`\n\nClaude Code `/compact` 对标。长会话(尤其 `--resume`)的消息历史会一直增长直到撞\nprovider 的 context window。两条路:**运行中自动压缩**(headless 默认开)+ **手动压缩\n存档会话**(`cc compact`)。引擎是现成的 `PromptCompressor`(snip+dedup+collapse+\ntruncate),**离线确定性**(不接 LLM 摘要,无网络调用)。`0.162.33+` 出。\n\n### 运行中自动压缩(`agent -p` / `--resume`)\n\n`agentLoop` 在每轮迭代顶部(LLM 调用**之前**、工具对完整的干净边界)检查历史是否超过\n压缩器阈值,超了就就地压缩。\n\n- **默认开**,由 `PROMPT_COMPRESSOR` 开关 + 大小阈值(按 `--model`/`--provider` 的\n context window 自适应)守门——只对真正大的上下文触发。\n- **退出**:`options.autoCompact === false`。交互式 REPL 走这条(它按自己的节奏压缩)。\n- **工具对安全**:截断 / snip 绝不会留下孤儿 `tool` 结果或无应答的 `tool_calls`\n (`compress(msgs, { preserveToolPairs:true })` → `sanitizeToolPairs()` 修复配对),\n 否则严格 API 会 400。\n- **`--resume` 持久化**:压缩后**自动写一条 `compact` 事件**到 JSONL 会话,下次\n `--resume` 直接从压缩后的短历史重建。**仅当会话已落盘**(session 文件存在)才写——\n 一次性 `cc agent -p`(不建会话文件)什么都不写。\n- `stream-json` 输出 `{ \"type\":\"compaction\", \"stats\":{ strategy, originalMessages,\n compressedMessages, originalTokens, compressedTokens, saved, ratio }, runId }`;\n `text` 态打印 `⊟ compacted context: 54→6 msgs (saved 879 tokens, dedup)`;`json`\n 态只出最终 result 信封,看不到中间事件。\n\n```bash\n# 长会话续跑——历史超阈值时本轮自动压缩并写 compact 事件,LLM 拿到的是压缩后上下文\nchainlesschain agent -p \"继续\" --resume --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact ` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume ` 自动拿短历史。\n\n```bash\nchainlesschain compact # 压缩并持久化\nchainlesschain compact --dry-run # 只预览缩减量,不写\nchainlesschain compact --json # 机读\nchainlesschain compact --model --provider

# 自适应阈值按指定模型\nchainlesschain compact --max-tokens --max-messages # 硬阈值覆盖\n```\n\n- 默认按会话**记录的** model/provider 给压缩器定 context-window 阈值;`--model`/\n `--provider` 或硬 `--max-tokens`/`--max-messages` 覆盖。\n- 历史已在阈值内 → 报 `Nothing to compact` 且**不写**任何事件。\n- 区别于 `cc checkpoint`(文件状态)与 `cc workflow checkpoint`(执行状态)。\n\n## Loop — 定时重复跑一条命令 / agent prompt\n\n`cc loop` 把**一件事**按固定间隔重复跑,直到停止条件命中或 Ctrl-C(Claude-Code `/loop`\n对标)。比 `cc ccron`(只在内存里治理 profile 状态机,**不真正执行**)和 `cc automation`\n(DB 持久的 flow/trigger 引擎)都轻——只是给一条命令套个计时器。\n\n```bash\n# 两种模式,用字面 `--` 分隔符区分:\nchainlesschain loop \"检查 CI 是否通过,失败就总结原因\" # 无 `--` → prompt,内部跑 `cc agent -p`\nchainlesschain loop --every 30s -- npm test # 有 `--` → 外部命令(shell 解析,Win 的 npm.cmd 也能跑)\nchainlesschain loop --every 1m --max-iterations 10 -- npm test\nchainlesschain loop --every 30s --until-exit-zero -- npm test # 测试一过就停\nchainlesschain loop --every 1m --until \"DONE\" \"轮询部署状态\" # 输出匹配正则就停\nchainlesschain loop --json --every 5m -- npm test # 结束打印 JSON 汇总\n```\n\n- **间隔** `--every `:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until `(某轮输出匹配 JS 正则就停)/ Ctrl-C(优雅停——\n 跑完当前轮、杀掉子进程)。退出码镜像最后一轮。\n- 轮间 sleep,**最后一轮后不 sleep**;`--until` 会捕获并 tee 子进程输出(既给你看实时输出,\n 又能匹配)。\n\n### Prompt 模式的 flag 透传\n\nprompt 模式下,**首个 `-…` 形 token 之前**的操作数当 prompt,**从首个 `-…` 起**的全部\n原样转发给 `cc agent`:\n\n```bash\nchainlesschain loop \"review the diff\" --think --provider openai\n# → cc agent -p \"review the diff\" --think --provider openai\n```\n\nloop 自己的 flag(`--every` 等)被 Commander 在 prompt 之前消费,放 prompt 前即可。\n\n### `--dynamic` — 让每轮自定步调\n\n每轮通过**输出里的指令**决定下一步,而不是固定间隔。loop 解析该轮输出:\n\n| 指令 | 含义 |\n|---|---|\n| `[[loop:next ]]` | 下一轮在 `` 后跑(如 `30s` / `5m` / `1h`) |\n| `[[loop:stop]]` | 任务完成 → 停(`stop` 优先于 `next`) |\n| 都不发 | 回退到 `--every` 默认间隔 |\n\n```bash\nchainlesschain loop --dynamic \"盯着部署,上线了就停\" # prompt 模式:prompt 被自动追加指令契约,agent 自己驱动节奏\nchainlesschain loop --dynamic --every 30s -- ./poll.sh # exec 模式:命令自己 println 指令;30s 是回退\n```\n\n- **prompt 模式**会把指令契约追加进 prompt,LLM 据此自报下次间隔或收尾;**exec 模式**由命令\n 自己打印指令。\n- `runLoop` 据此收到 `done`(停止理由 `done`)与逐轮 `nextDelayMs`;协议解析器\n `parseLoopDirectives` 独立可单测。\n\n> ⚠️ **exec 模式用 `shell:true`**(让 `npm test` 这类 `.cmd` shim 能解析),因此**带空格的单个参数**\n> 会被 shell 重新切分——这是标准 shell 行为。prompt 模式与 `--` 后的简单命令不受影响。\n\n### `--save` / `--resume` — 会话持久化 + 续跑\n\n把一个 loop 持久成 JSONL 会话(写到 `~/.chainlesschain/sessions/.jsonl`),之后可续跑:\n\n```bash\nchainlesschain loop --save ci-watch --every 1m -- npm test # 持久成可续跑会话\nchainlesschain loop --resume ci-watch --max-iterations 20 # 续跑(累计计数)\n```\n\n- **`--save [id]`** 写三类事件:一条 `loop_config`(模式 / 操作数 / 间隔 / 停止条件)+ 每轮一条\n `loop_iteration`(退出码 / 耗时 / dynamic 字段,**不存输出体**,保持会话小)+ 收尾 `loop_end`。\n 省略 `id` 时自动生成;结束时打印 `resume with: cc loop --resume `,`--json` 里带 `sessionId`。\n- **`--resume `** 回放 `loop_config`(**重建原命令/prompt,不必重打**)、数已完成轮数,然后\n **累计续跑**——`--max-iterations` 跨续跑累计(saved 2 轮 + resume 到 4 = 再跑 2 轮)。\n- **续跑时显式重传的 flag 覆盖存档**(如 `--max-iterations 20` 扩预算、`--every 30s` 改间隔),\n 靠 Commander `getOptionValueSource` 区分\"用户显式传\"与\"默认值\";没传的继承存档。\n- 纯函数 `summarizeLoopEvents(events) → {config, completedIterations, lastExitCode}` 独立可单测。\n\n> 💡 **headless 必须真等待**:loop 的轮间 sleep 计时器**不 unref**,否则 stdin 非 TTY(管道 / CI /\n> cron)时事件循环没东西吊着,会在第一轮后就退出。SIGINT 仍能经 abort signal 中断等待。\n\n## MCP — 把工具喂给 agent 循环\n\n两条路把 MCP server 的工具暴露给 LLM,工具名统一 `mcp____`,经 agent-core\n默认分派走 `mcpClient.callTool()`。**headless + 交互** 都支持,二者合并进**同一个 client**\n(同名时 ad-hoc 优先)。\n\n```bash\n# 1) 临时(ad-hoc):一次性从 JSON 文件挂载 —— Claude Code --mcp-config 对标\nchainlesschain agent -p \"纽约天气?\" --mcp-config ./mcp.json\n# mcp.json 形如 { \"mcpServers\": { \"weather\": { \"command\": \"npx\",\n# \"args\": [\"-y\",\"@modelcontextprotocol/server-weather\"] } } }\n\n# 2) 注册(持久):cc mcp add --auto-connect 的 server 每次 cc agent 自动连\nchainlesschain mcp add weather -c npx -a \"-y,@modelcontextprotocol/server-weather\" --auto-connect\nchainlesschain agent -p \"纽约天气?\" # weather 工具自动可用\nchainlesschain agent -p \"...\" --no-mcp # 跳过注册的自动连接(--mcp-config 仍生效)\n```\n\n**`--permission-prompt-tool `**(需配 `--mcp-config`):headless 默认对中/高危\n工具 fail-closed,本旗标把每个 CONFIRM 档审批交给一个 MCP 工具裁决——它收 `{tool_name, input}`,\n返回 `{behavior:\"allow\"|\"deny\"}`(任何非 allow / 出错都 fail-closed)。\n\n### Resources — 把 MCP 资源喂给 agent\n\nMCP 不止 tools。当任一连上的 server 暴露 **resource**(文档 / 文件 / 数据)时,agent\n循环自动多两个通用工具(对标 Claude Code 的 `ListMcpResourcesTool` / `ReadMcpResourceTool`):\n\n- `list_mcp_resources {server?}` → 列出所有(或某 server 的)资源 `{server, uri, name, ...}`\n- `read_mcp_resource {uri, server?}` → 按 URI 读取内容;省略 `server` 时按 uri 自动定位归属\n\n只有\"存在资源\"才注册这两个工具(无资源的纯工具 server 不会污染工具表);多批合并(`--mcp-config`\n+ 注册 + IDE)只注册一次。CLI 侧对应 `cc mcp resources` / `cc mcp read-resource `(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp____`(对标 Claude Code):\n\n```text\n/mcp # 总览:列出所有已连 server 的资源 + prompt\n/mcp__docs__summarize {\"len\":\"short\"} # 取回该 server 渲染好的 prompt,作为本轮输入喂给 LLM\n/mcp__docs__greet 直接跟一段文本 # 非 JSON 尾巴 → 作为 { input: \"...\" } 传入\n```\n\nCLI 侧对应 `cc mcp prompts` / `cc mcp get-prompt `。\n\n## IDE Bridge — 自动连接编辑器的 MCP server (Phase 0)\n\n第三条 MCP 源:运行中的编辑器扩展(VS Code / JetBrains)可在内部跑一个本地 MCP server\n(暴露 `ide_getSelection`/`ide_getDiagnostics`/`ide_openDiff` 等\"IDE 工具\"),并把它的端点写进\n一个 **lockfile**。`cc agent` 自动发现并把它当保留名 `ide` 的 server 连上,工具即\n`mcp__ide__getSelection` 等,与 `--mcp-config` / 注册 server 合并进同一 client。设计见\n`docs/design/modules/98_IDE桥接对标方案.md`。**Phase 0 = 纯 CLI 发现层**(扩展为后续阶段)。\n\n**发现两条路径**(`src/lib/ide-bridge.js`):\n- **env 直连(主)**:扩展在它拥有的集成终端注入 `CHAINLESSCHAIN_IDE_PORT`(+ 可选\n `CHAINLESSCHAIN_IDE_TOKEN`)→ CLI 直接锁定该 port 的锁,免扫描、免歧义。\n- **lockfile 扫描(兜底)**:扫 `~/.chainlesschain/ide/*.json`,按 `workspaceFolders[]` 与\n `cwd` 的**最长前缀**匹配挑一条(多匹配取 `started_at` 最新)。\n\n**Lockfile**(`~/.chainlesschain/ide/.json`,文件 `0600` / 目录 `0700`):\n```jsonc\n{ \"ide\":\"vscode\", \"transport\":\"sse\", \"url\":\"http://127.0.0.1:53690/sse\",\n \"port\":53690, \"workspaceFolders\":[\"/abs/ws\"], \"token\":\"\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer `;`pid` 不存活且文件超 30s 视为 stale 跳过。\n\n```bash\nchainlesschain agent -p \"修下选区里的 bug\" # 在 IDE 集成终端内自动连接\nchainlesschain agent -p \"...\" --ide # 强制启用(外部终端手动接)\nchainlesschain agent -p \"...\" --no-ide # 禁用 IDE 自动连接\n\nchainlesschain ide list # 列出发现到的 IDE server(token 不显)\nchainlesschain ide status [--ide] # 此刻会连哪台 + 它的 MCP config(token 脱敏)\nchainlesschain ide doctor [--ide] # 解释发现为何成功/失败(无锁/stale/workspace 不匹配/…)\n```\n\n**交互 REPL `/ide`**(Claude-Code 平价):在 `chainlesschain agent` 交互会话里输入 `/ide` 查看**本会话**的桥接状态——已连则显示编辑器/端口/工作区 + 可用 `mcp__ide__*` 工具;未连则给发现诊断(几个锁、为何不匹配)+ 怎么连(从 IDE 集成终端跑、或 `--ide`)。等价于 `cc ide doctor` 的会话内即时版。\n\n**保留名 `ide`**:若你已 `cc mcp add ide` 自建同名 server,**用户显式注册优先**,IDE 自动发现让位\n并打印一次 WARN。\n\n> 端到端自验(免扩展):`cc mcp scaffold` 生成一个 SSE MCP server 当\"假 IDE\",手写一条\n> `.json` 锁指向它,跑 `cc agent --ide` 即可确认 `mcp__ide__*` 工具进 loop + Bearer 鉴权通。\n\n> **热重连**:窗口 reload / 扩展升级让 IDE 的 MCP server 换新端口+新 token 重启时,**不需要**\n> 重跑 `cc agent`——下一次 `mcp__ide__*` 调用失败会自动重扫 lockfile、连上新实例并重试一次\n> (选区/诊断自动注入同样恢复);IDE 彻底关掉则该次调用报错,工具级错误不会触发重连。\n> headless(`agent -p`)、stream、交互 REPL 三个入口均已接 IDE 自动连接。\n\n### IDE 实时上下文 + 编辑后诊断回喂(Claude-Code 平价)\n\nIDE 桥连上后,agent 不再只是\"有 IDE 工具可调\",还会**主动感知编辑器状态**:\n\n- **提交时自动共享选区/打开文件**:每次提交 prompt(headless 单轮、stream 每轮、REPL 每条),\n CLI 自动调一次 `getSelection` + `getOpenEditors`,把活跃文件、打开的 tab、当前选中代码\n (截断到 2000 字符)作为 `` 块附进**本轮用户消息**——模型无须自己决定调工具。\n 该块是**短暂的**:只进在途消息,不写入会话持久化(resume 回放的是你的原话,不是过期的\n 编辑器快照)。IDE 无响应时 1.5s 超时放弃,绝不阻塞回合。\n- **编辑后诊断自动回喂**:`write_file`/`edit_file`/`edit_file_hashed` 成功后,CLI 等\n 语言服务器消化变更(默认 600ms,`CC_IDE_DIAG_SETTLE_MS` 可调,`0` 跳过),拉取该文件的\n **error/warning 诊断**(info/hint 不打扰),作为 `ideDiagnostics` 附在工具结果里——模型\n 在**同一个循环**里就能看到自己刚引入的报错并修掉,而不是若干轮后才发现。最多列 10 条。\n\n两者共用开关:`CC_IDE_CONTEXT=0` 一并禁用(IDE 工具本身仍可被模型显式调用)。\n\n- **`@selection` / `@diagnostics` at-mention(显式版)**:除了上面\"每轮自动共享\"的隐式注入,\n 你也可以在 prompt 里**显式**写 `@selection` 把当前选区拼进本轮,或写 `@diagnostics` 把\n **整个 workspace 的当前 error/warning 问题**拼进来(后者是自动注入没有的——自动诊断只针对\n 刚编辑的单个文件)。与 `@path` 文件引用同一语法边界(`foo@selection`、邮箱不会误匹配),\n 在 headless 单轮、stream 每轮、交互 REPL 三入口均生效。展开同样**短暂**(只进在途消息,不写\n 持久化)。未连 IDE / 无选区 / 无问题时,`@mention` 原样留在文中并打印一条 `@ide:` 提示。\n 与隐式注入不同,显式 at-mention **不受 `CC_IDE_CONTEXT=0` 影响**(它是你的直接请求)。\n\n- **编辑审批走 IDE 原生 diff**:settings 权限规则对 `Write`/`Edit` 配了 `ask` 且在**交互\n REPL** 里(headless 保持 fail-closed 不弹)、IDE 已连时,确认不再是终端 y/N,而是编辑器\n 弹出**原生并排 diff**(右侧可编辑)阻塞到你裁决:**Accept → IDE 自己把(可能被你顺手改过\n 的)最终文本写盘,工具自身的写入被跳过**(审批替代执行,杜绝双写);Reject → 拒绝且文件\n 不动;IDE 中途挂掉/无法计算拟议内容 → 自动回落终端确认。`CC_IDE_DIFF_APPROVAL=0` 关闭\n 此路由(回终端 y/N)。\n\n## Web Search — `web_search` 工具 + 可插拔搜索源\n\nagent 循环原生带 `web_search`(配合 `web_fetch`:先搜出 URL,再抓正文)。返回归一化\n`{ query, provider, count, results:[{title,url,snippet}], answer }`。搜索后端**可插拔**,\n经 `.chainlesschain/config.json:webSearch` 配置——搜索源是一个配置开关,不是一次性写死的代码决定。\n\n```jsonc\n// .chainlesschain/config.json\n{\n \"webSearch\": {\n \"provider\": \"auto\", // auto | tavily | brave | bocha | qianfan | duckduckgo | searxng | baidu\n \"apiKey\": \"\", // 通用 key;也可用 tavilyApiKey/braveApiKey/bochaApiKey/qianfanApiKey 分别指定\n \"maxResults\": 8,\n \"instanceUrl\": \"\", // 仅 searxng:自建实例地址(需开 json 输出格式)\n \"qianfanApiKey\": \"\", // 百度千帆 AI 搜索 Bearer token,完整 bce-v3/ALTAK-.../ 形式(见下)\n \"qianfanUrl\": \"\" // 可选:覆盖千帆 AI 搜索 endpoint,默认 https://qianfan.baidubce.com/v2/ai_search\n }\n}\n```\n\n- **`provider: auto`(默认)**:按 `tavily > brave > bocha > qianfan` 顺序选用**已配置 key** 的那个;\n 都没 key 时退回**免 key** 的 DuckDuckGo(HTML 端点解析,质量/稳定性较弱)。\n- **API key 来源**(优先级 options > config > env):`TAVILY_API_KEY` / `BRAVE_API_KEY`\n (兼容 `BRAVE_SEARCH_API_KEY`)/ `BOCHA_API_KEY` / `QIANFAN_API_KEY`。Bocha(博查)为国内可达的 AI 搜索源。\n- **`baidu`(免 key,国内可达)**:抓 `www.baidu.com/s` HTML(浏览器 UA)。适合国内\n 低频检索(DuckDuckGo 国内常被墙);但 Baidu 反爬激进,频繁请求会 302 跳验证码,此时\n 返回明确的 `rate-limited / captcha` 错误而非静默失败。高频/稳定需求建议用 keyed 源。\n- **`qianfan`(百度千帆 AI 搜索,keyed,国内可达稳定)**:Baidu 官方检索增强 API,\n 比免 key 抓 HTML 稳定可靠(无验证码),适合 agent 高频检索。**配置**:把 Bearer token\n 填进 `webSearch.qianfanApiKey`(或环境变量 `QIANFAN_API_KEY`);需要换 endpoint 时填\n `webSearch.qianfanUrl`。\n - 取 token:[百度智能云千帆控制台](https://console.bce.baidu.com/qianfan/) → 开通「AI 搜索」→\n 创建 Bearer token。**必须是完整 `bce-v3/ALTAK-.../` 形式**;只填短 API Key 片段\n 会 401 `InvalidHTTPAuthHeader: Fail to parse apikey authorization`。\n - 请求体固定为 `{messages:[{role:\"user\",content:query}], search_source:\"baidu_search_v2\",\n resource_type_filter:[{type:\"web\",top_k:maxResults}]}`,解析返回的 `references[]`\n 成 `{title,url,snippet}`(并捕获可能的 `answer`)。\n - 用法:`webSearch(q,{provider:\"qianfan\"})` 或 config `\"provider\":\"qianfan\"`。\n > ✅ RUNTIME-VERIFIED(2026-06-09):用真 `bce-v3/…` token 实测,\"杭州亚运会举办时间\" →\n > 5 条正确结果(`references[]` schema 解析正确,百度百科/一带一路网等),live 测试通过。\n- **keyed 后端 live 测试**:`__tests__/integration/web-search-live.test.js` 对 tavily/brave/\n bocha/qianfan 各跑一条**真实查询**——**仅当对应 env key 存在时执行,否则自动 skip**(不失败)。\n 例:`BOCHA_API_KEY=sk-xxx npx vitest run __tests__/integration/web-search-live.test.js`。\n- 单次调用可用工具入参 `provider` 覆盖配置后端;`web_search` 属 `readonly`,plan-mode 允许,\n 权限规则伞名 `WebSearch`(见上「Permissions」表)。\n- 已注册的 `brave-search` MCP(`cc mcp`)是另一条搜索路径,但需逐用户安装+配 key;\n `web_search` 是内建、开箱即用的等价能力。\n\n## Subagents — `cc agents` + spawn_sub_agent 委派\n\n`.chainlesschain/agents/*.md`(原生,优先)或 `.claude/agents/*.md`(Claude Code 可移植)\n定义命名子代理:**正文 = system prompt**,frontmatter 声明 `description` / `tools`(白名单)/ `model`。\n`review/security.md` → 子代理 `review:security`。\n\n```bash\nchainlesschain agents list # 列出发现的子代理\nchainlesschain agents show review:security [--json] # 看元数据 + system prompt\nchainlesschain agents run review:security \"审计这个 diff\" # 以该子代理身份单轮 headless 跑\nchainlesschain agents new reviewer --tools \"read_file,search_files\" # 脚手架(默认 .chainlesschain/,--claude 落 .claude/)\n```\n\n`cc agents run` 把子代理(正文=system prompt + frontmatter tools/model)映射到 `runAgentHeadless`,\n所以一份 Claude Code agent 定义可原样跑。\n\n**主 agent 主动委派**:agent 循环里的 `spawn_sub_agent` 工具新增 `agent` 参数——传子代理名即\n加载它的 system prompt + 工具白名单 + `model:`(子代理继承父的 provider/key,只覆盖 model);\n显式 `role`/`tools` 仍优先;给了 `agent` 时 `role` 可省。\n\n## Context — `cc context` 上下文窗口占用\n\nClaude Code `/context` 对标:看某存档会话把模型上下文窗口占了多少,按角色拆分。\n\n```bash\nchainlesschain context [] # 省略=最近一次 headless 会话\nchainlesschain context --json\nchainlesschain context --model claude-sonnet-4-6 # 按指定窗口估\n```\n\n按 system / user / assistant / tool(含 tool_calls)分桶给 token 数 + 占比 + 剩余余量;\n默认从会话 `session_start` 头自动识别 model/provider(`--model`/`--provider` 覆盖)。复用自动压缩\n的 token 估算器 + 窗口表,不采新数据。补足 `cc cost`($)+ `cc session usage`(裸 token 数)。\n\nREPL 内有同款 **`/context`**:对**当前活跃会话**(内存中的 messages)实时分桶,不用先存档。\n\n## Session Export / REPL 补全 / 版本提醒(小件平价)\n\n- **`cc session export [-o file]`** — 会话导出 Markdown。主源仍是聊天 DB 会话;\n 查不到时自动回退 **JSONL agent 会话**(`cc agent --resume` 那套):user/assistant/system 轮次 +\n tool_call/tool_result 围栏块(4K 截断,内嵌 ``` 自动升级围栏)+ compact 标记 + token 汇总。\n `last` 直接导最近一次 agent 会话(不碰 DB)。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he` 补全注册的 REPL 命令(命令 token 期间生效,\n 空格后不打扰参数);与 `@` 文件补全共存于同一 completer。\n- **启动版本提醒** — 每次启动一次性同步读缓存(`~/.chainlesschain/update-check.json`),有新版时\n stderr 打一行灰字(仅 TTY,不污染管道/JSON);缓存 >24h 由 detached 子进程后台刷新供下次启动,\n 热路径零网络。`CC_UPDATE_NOTICE=0` 关闭;完整检查仍走 `cc update`。\n\n## Structured Output — `--json-schema `(headless)\n\n```bash\ncc agent -p \"列出本目录的语言构成\" --json-schema schema.json\n```\n\n最终回答必须是通过该 JSON Schema(子集:type/properties/required/items/enum/const/\nadditionalProperties)校验的 JSON:模型回复自动提取(裸/围栏/嵌入三种形态),校验失败带错误\n清单纠错重跑(共 3 次),stdout **只打印校验通过的 JSON**(中间过程进 stderr),耗尽返回退出码 1。\n与 `--output-format stream-json` 互斥。脚本消费 `cc agent -p` 从此不用 parse 自由文本。\n\n## GitHub Action — `@cc` 评论驱动 agent(claude-code-action 平价 v1)\n\n`.github/workflows/cc-agent-mention.yml`:在 issue / PR 评论里写 `@cc <问题>`,\nheadless `cc agent` 以**只读工具**(read_file/list_dir/search_files)分析代码并回评。\n\n- **触发门控**:仅 OWNER/MEMBER/COLLABORATOR 的评论触发(陌生人 prompt 注入到不了 agent);\n PR 评论自动 checkout PR 分支。\n- **配置**:repo secret `CC_LLM_API_KEY`(必填,缺失时回评说明并真实失败,不假绿)+\n repo variables `CC_LLM_PROVIDER`/`CC_LLM_MODEL`(默认 volcengine/doubao)。\n- **v1 边界**:只回答不改码(无 write 工具、`--no-checkpoint --no-mcp`,15 轮上限,15 分钟超时);\n 改码+推 commit 是 v2。其它仓库复制该 workflow 文件即可用。\n\n## REPL Steering — 排队输入 / Esc 中断 / rewind(Claude-Code 平价)\n\nagent REPL 运行 turn 期间的三件交互能力:\n\n- **排队输入**:turn 进行中继续打字回车,不再并发触发第二个 turn(旧行为是个 race),而是\n FIFO 排队(灰字 `⏸ queued (N)` 提示),当前 turn 结束自动逐条续跑。\n- **Esc 中断**:turn 进行中按 Esc 立即中止本轮(走 agentLoop 既有 AbortSignal seam,每迭代\n 检查),黄字提示、partial 对话保留、排队的输入照常 drain。方向键等转义序列不会误触。\n- **`/rewind` + 双 Esc**:`/rewind` 列出最近 user 轮次(新在前带预览),`/rewind ` 把对话\n 截断回该轮之前并把原文回填到输入行(改完重发);**空闲时双击 Esc** 是同款列表的快捷键。\n 只回滚对话态;文件态用 `cc checkpoint restore`(配 `cc agent --checkpoint` 自动快照)。\n\n## Project Memory — `cc.md` 项目记忆自动加载 + `cc init` 盘点\n\nClaude Code CLAUDE.md 体系对标(主名用自家 `cc.md`,设计文档 `docs/design/modules/99_项目记忆与init对标方案.md`)。`cc agent` 启动时自动把文件式项目约定注入系统提示(`` 块):\n\n| 范围 | 查找顺序(每位置取第一个存在的) |\n|---|---|\n| 用户级 | `~/.chainlesschain/cc.md` → `~/.claude/CLAUDE.md` |\n| 项目级(git root → cwd 逐目录) | `cc.md` → `CLAUDE.md` → `AGENTS.md` |\n| 本地伴随(每目录) | `cc.local.md` → `CLAUDE.local.md` |\n| 脚手架规则(每目录) | `.chainlesschain/rules.md` |\n\n- 文件内 `@相对路径` / `@~/路径` 递归 import(深度 ≤5,环保护,跳过代码围栏;npm scope/邮箱等解析不到文件的 token 静默忽略)\n- 预算:单文件 48KB / 总 192KB,超限截断标记;任何 I/O 错误 fail-open 不影响启动\n- 关闭:`CC_PROJECT_MEMORY=0`(全局)\n\n```bash\nchainlesschain init # 默认:盘点当前文件夹 → 生成 cc.md + 最小 .chainlesschain/(config + skills/)\nchainlesschain init --force # 覆盖已有 cc.md\nchainlesschain init -t <模板> -y # 显式模板:老脚手架流(persona/skills/rules)\n```\n\n盘点为纯离线 census(语言构成 / lockfile 包管理器 / scripts / workspaces / 工具链标记 / CI / README 摘要),不读源码内容。已有 CLAUDE.md/AGENTS.md 时自动在生成的 cc.md 里 `@import` 引回,不会遮蔽手工维护的内容。\n\n**REPL 配套前缀**:\n\n- `! ` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`/`)\n- `# ` — 一键记到 git root 的 `cc.md`(`## Notes` 段,最新在上),本 session 立即生效、下 session 自动加载\n\n## Permissions — `.claude/settings.json` 规则 (allow / ask / deny)\n\nClaude Code `permissions.{allow,ask,deny}` 对标。给 `cc agent`(headless + 交互\nREPL)加一层**用户可编辑的 glob 规则**网关:在工具真正执行前,按规则 `deny` 硬拦 /\n`ask` 要确认 / `allow` 预授权。无 settings.json 时行为**逐字不变**(回落到既有的\nrisk-tier / ApprovalGate / plan-mode 逻辑)。引擎零依赖(自写 glob→regex)。\n\n### 规则语法 `Tool(pattern)`\n\n裸 `Tool` 匹配该工具的所有调用;`Tool(pattern)` 按工具类型匹配实参。工具名两种写法\n都吃——Claude-Code 伞名或本 CLI 原名(双向别名):\n\n| 伞名 | 本 CLI 工具 | `pattern` 匹配对象 | 例 |\n| ---- | ----------- | ------------------ | -- |\n| `Bash` | `run_shell` | 命令字符串 | `Bash(git push:*)`、`Bash(npm run test:*)` |\n| `Read` | `read_file` / `list_dir` | 解析后的绝对路径 | `Read(./src/**)` |\n| `Edit` | `edit_file` / `edit_file_hashed` | 路径 | `Edit(//etc/**)`(`//`=绝对) |\n| `Write` | `write_file` | 路径 | `Write(./build/**)` |\n| `WebFetch` | `web_fetch` | URL host | `WebFetch(domain:example.com)` |\n| `WebSearch` | `web_search` | — | `WebSearch` |\n| `Task` | `spawn_sub_agent` | — | `Task` |\n\n- **命令**:`prefix:*` 前缀语义(`Bash(git push:*)` 命中 `git push` 及其后任意参数);\n 纯 `*` 当 glob;无 `*` 则精确匹配。\n- **路径**:`./x` 相对 cwd、`//abs` 绝对、`~/x` home;`*` 不跨 `/`、`**` 跨。\n- 未知工具名(如 MCP `mcp__srv__do`)按精确名匹配。\n\n### 文件层级与合并\n\n按优先级**并集**合并(高层只增规则,`deny` 永不被稀释);坏 JSON **fail-open**(告警跳过,\n绝不让 agent 卡死):\n\n```\n~/.claude/settings.json # user(所有项目)\n/.claude/settings.json # project(签入)\n/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings # cc agent 显式传入\nCC_PERMISSIONS_ALLOW / _ASK / _DENY # env kill-switch(逗号分隔)\n```\n\n### 判定顺序(most-restrictive-wins,denies 先于任何 prompt)\n\n1. **settings `deny`** → 拦。\n2. **host policy `deny`**(`hostManagedToolPolicy`,桌面运行时同步,CLI 通常无)→ 拦。\n settings `allow` **永不** relax host deny;settings `deny` 盖 host `allow` → **任一 deny 即拦**。\n3. **settings `ask`** → 确认。**在两个 deny 之后**才到——host-denied 工具不会白白弹一次\n 确认。headless 无确认器 → fail-closed。\n4. **settings `allow`** → 预授权:短路 plan-mode 拦截 + `run_shell` 的 ApprovalGate 档位\n 确认。**安全不变量**:`allow` 不会重开 shell 硬黑名单(`curl`/`rm` 等仍被\n shell-policy 拦)。\n\n### `cc agent --settings`\n\n`--settings ` 合并一份 `.claude/settings.json` 形状的文件,作用于本次运行。\n除权限规则外,还能做**原生配置覆盖**(`model` + `env`),无需改 `.chainlesschain/config.json`:\n\n```bash\nchainlesschain agent -p \"跑测试\" --settings ci-perms.json\n# ci-perms.json 形如 { \"permissions\": { \"allow\": [\"Bash(npm run test:*)\"],\n# \"deny\": [\"Bash(rm:*)\"] } }\nchainlesschain agent -p \"大重构\" --settings run.json\n# run.json 形如 { \"model\": \"claude-opus-4-8\",\n# \"env\": { \"OLLAMA_HOST\": \"http://gpu-box:11434\" } }\n```\n\n- **`model`**:填本次运行模型——仅当未显式给 `--model` 时生效(`--model` 优先)。\n- **`env`**:逐 key 设进 `process.env`,agent 循环 + 子进程工具都继承。\n- 优先级与权限一致:`~/.claude` < 项目 `.claude` < `.local` < `--settings` 文件(后者覆盖前者,\n `env` 逐 key 合并)。`--permission-mode` 仍叠加在权限规则**之后**(规则未命中才看档位)。\n\n### `cc permissions` — 查看 / 干跑 / 编辑\n\n```bash\nchainlesschain permissions list [--json] # 合并后规则集 + 每条来源文件\nchainlesschain permissions test run_shell \"git push origin main\" # 干跑:命中哪条规则→什么判定\nchainlesschain permissions test read_file src/app.js --json\nchainlesschain permissions add deny \"Bash(rm:*)\" # 追加规则(默认 project)\nchainlesschain permissions add allow \"Read(./**)\" --local # → settings.local.json(个人)\nchainlesschain permissions add allow \"Read\" --user # → ~/.claude/settings.json\n```\n\n**交互 REPL `/permissions`**(Claude-Code 平价):在 `chainlesschain agent` 会话里输入 `/permissions` 查看**本会话生效**的 allow/ask/deny 规则(按 deny>ask>allow 优先级排列)+ 来源文件 + \"危险 shell 命令永远需批准\"提示。会话期间用 always-allow 现加的规则也即时反映。等价 `cc permissions list` 的会话内即时版。\n\n- `test` 是调规则的杀手锏——执行前看清某调用会被哪条规则、哪个文件决定(`fallthrough`=\n 无规则命中,回落档位逻辑)。伞名 `Bash` 会解析成具体工具 `run_shell`。\n- `perm` 已被 RBAC Permission Engine V2 占用,故权限规则命令用全名 `permissions`。\n\n### 交互式 \"always allow\"(REPL)\n\nREPL 里 `run_shell` 撞 ApprovalGate 风险确认时,提示是 **`[y]es once / [a]lways allow /\n[N]o`**。选 `[a]`:从该调用推导一条规则(`git push origin main` → `Bash(git push:*)`),\n写入 `.claude/settings.local.json` 并即时更新内存规则集 → **本/未来 session 不再问**(经\n上面的 `allow` 短路)。显式 `ask` 规则保持 y/N(asking 是声明意图;且 deny>ask>allow 下\nallow 也盖不过 ask)。\n\n## Hooks — `.claude/settings.json` `hooks` 块 (Claude-Code 协议)\n\nClaude Code hooks 对标。两个来源,职责不同:\n\n- **DB hooks**(`cc hook add` → SQLite)—— **observe-only**,触发即记录,不参与决策(行为不变)。\n- **`.claude/settings.json` `hooks` 块** —— **决策型**:走 stdin-JSON + 退出码/JSON-stdout 协议,可**拦截**工具调用。本节讲后者。\n\n### Schema\n\n```json\n{\n \"hooks\": {\n \"PreToolUse\": [\n { \"matcher\": \"Bash\",\n \"hooks\": [ { \"type\": \"command\", \"command\": \"./guard.sh\", \"timeout\": 60 } ] }\n ],\n \"PostToolUse\": [\n { \"matcher\": \"Edit|Write\", \"hooks\": [ { \"type\": \"command\", \"command\": \"./fmt.sh\" } ] }\n ]\n }\n}\n```\n\n- `matcher`:工具名匹配,支持精确 / `Edit|Write` 管道 / `*` 通配 / `/regex/`。对**伞名**(`Bash`)和**本 CLI 原名**(`run_shell`)都测——两种写法都命中。\n- hooks 数组**跨层级拼接**(user < project < .local < `--settings`),有序、不去重。\n- `timeout` 秒(默认 60)。\n\n### 协议(命令 hook)\n\n事件 payload 以 **JSON 写入命令 stdin**(`{ hook_event_name, tool_name, tool_input, cwd, session_id }`)。判定:\n\n| hook 返回 | 含义 |\n| --------- | ---- |\n| **exit 2** | **block**(reason = stderr)——标准\"拒绝\"路径 |\n| exit 0 + stdout `{\"decision\":\"block\"\\|\"approve\"\\|\"ask\",\"reason\"}` | 按之 |\n| exit 0 + `{\"hookSpecificOutput\":{\"permissionDecision\":\"deny\"\\|\"allow\"\\|\"ask\"}}` | PreToolUse 专用 |\n| exit 0 + `{\"continue\":false,\"stopReason\"}` | block |\n| exit 0(无 JSON)| continue(放行;stdout 可作 `additionalContext`)|\n| 其它非零 / spawn 失败 / 超时 | **非阻塞**(surface,不拦——坏 hook 不卡 agent)|\n\n多个命中的 hook 按序跑,**首个 block/ask 短路**。\n\n### `cc hook test` — 干跑\n\n```bash\nchainlesschain hook test PreToolUse run_shell \"git push origin main\" # 列出会触发的 hook(不执行)\nchainlesschain hook test PreToolUse run_shell \"git push\" --run # 真执行 + 显示 decision/reason/exit\nchainlesschain hook test PreToolUse write_file \"x\" --json\nchainlesschain hook list # DB hooks + settings.json hooks(标来源)\n```\n\n默认**只列命中**(安全,不执行用户脚本);`--run` 才真跑并显示判定。是调 hook 的杀手锏。\n\n### 已接入的事件(在 `cc agent` 真实执行中强制)\n\n| 事件 | 触发点 | 能力 |\n| ---- | ------ | ---- |\n| **PreToolUse** | 每个工具调用前(权限解析之后) | `block`→拦工具 / `ask`→确认 / context |\n| **PostToolUse** | 工具跑完后 | `block` 的 reason 作 `hookFeedback` 回喂模型 |\n| **UserPromptSubmit** | 每轮用户 prompt 提交前(headless + REPL) | `block`→中止本轮 / 非 block stdout(JSON `additionalContext` 或纯文本)→注入为上下文 |\n| **SessionStart** | 会话起点(headless + REPL) | 非 block stdout → 注入为一条 `system` 上下文消息(observe-only,无 block);`source`=startup/resume 即 matcher 目标 |\n| **Stop** | agentLoop 收尾(agent 给出最终答复,无工具调用) | `block`→**强制续跑**(reason 作新指令注入,`stop_hook_active` 防死循环 + 迭代预算兜底);payload 含 `final_response`/`stop_hook_active` |\n| **SubagentStop** | `spawn_sub_agent` 工具完成后(子 agent 已返回摘要) | observe + feedback:子 agent 已结束故不\"强制续跑\",`block` 的 reason 作 `hookFeedback` 回喂**父** agent;payload 含 `subagent_response`/`session_id` |\n| **PreCompact** | 自动压缩前(`shouldAutoCompact` 命中) | `block`→**跳过本轮压缩**(emit `compaction-skipped`);payload 含 `trigger`/`message_count` |\n| **SessionEnd** | 会话结束(headless run 收尾 / stdin 关闭 / REPL 退出) | observe-only;payload 含 `reason` |\n| **Notification** | agent 需要用户注意时(REPL 弹权限/风险确认前) | observe-only(响铃 / 桌面通知);payload 含 `message`。仅交互 REPL(headless 无人可通知) |\n\nUserPromptSubmit payload:`{ hook_event_name, prompt, cwd, session_id }`;block 在 headless 退出码 `2`、在 REPL 跳过本轮。SessionStart payload:`{ hook_event_name, source, cwd, session_id }`。Stop/PreCompact 由 `agentLoop` 中心触发(覆盖三入口);SessionEnd 在各入口收尾触发;SubagentStop 与 PostToolUse 同在 `executeTool` 里触发(仅当工具名为 `spawn_sub_agent`)。settings/host `deny`(权限规则)先于 hook 短路,被拒的工具调用不会 spawn hook 进程。\n\n> **全事件已接入**:工具(PreToolUse/PostToolUse)+ prompt(UserPromptSubmit)+ 会话(SessionStart/SessionEnd)+ Stop(可 block→续跑)+ PreCompact(可 block→跳过压缩)。SessionEnd 为 observe-only。\n\n## Output Styles — `/output-style` 人格\n\nClaude Code `/output-style` 对标。给 agent 套一层**命名、可复用的人格**(persona),\n追加到系统提示之后——保留核心编码能力,但改变行为/语气。\n\n样式 = 带 frontmatter(`name`/`description`)的 markdown,**body 追加到系统提示**\n(在 base + `--append-system-prompt` 之后)。来源:`.chainlesschain/output-styles/`、\n`.claude/output-styles/`(项目)、`~/.claude/output-styles/`(个人);另含内置\n`default`(无叠加)/ `explanatory`(讲解 why+权衡)/ `learning`(留小块给用户写)。\n生效优先级:`--output-style` > `.claude/settings.json` 的 `outputStyle` 默认。\n\n```bash\nchainlesschain agent -p \"...\" --output-style explanatory # headless 套人格\nchainlesschain output-style list [--json] # 内置 + 文件,标 * = settings 默认\nchainlesschain output-style show explanatory\nchainlesschain output-style new pirate --description \"...\" # 脚手架到 .claude/output-styles/\n# .claude/settings.json: { \"outputStyle\": \"learning\" } # 项目默认\n```\n\nREPL 内:`/output-style`(列出 + 当前)、`/output-style `(切换,即时重算系统\n消息)、`/output-style none`(清除)。`--system-prompt` 仍可整体替换系统提示;output\nstyle 是其上的人格叠加层。\n\n## MCP OAuth — 远程 MCP server 授权\n\nClaude Code 远程 MCP OAuth 对标。需要 OAuth 的远程 HTTP/SSE MCP server 用\n`cc mcp login ` 授权一次,token 存盘并在每次连接时注入 `Authorization: Bearer …`\n(过期自动 refresh)。已持有 token 的 server 仍可用静态 `-H \"Authorization: Bearer …\"`。\n\n流程(标准 OAuth 2.0):RFC 9728/8414 元数据发现 → RFC 7591 动态客户端注册(或 `--client-id`)\n→ RFC 7636 PKCE → 开浏览器授权 → localhost 回调接 code → 换 token → 落盘\n`~/.chainlesschain/mcp-oauth.json`(按 server origin 索引)。\n\n```bash\nchainlesschain mcp login https://mcp.example.com/mcp # 开浏览器授权(动态注册客户端)\nchainlesschain mcp login --scope \"read write\" --client-id # 预注册客户端 / 指定 scope\nchainlesschain mcp login --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout # 删除 token\n```\n\n连接时:`setupMcpFromConfig` 对带 `url` 的 server,若有已存 token → `ensureValidToken`\n(到期且有 refresh_token 则刷新)→ 注入 Bearer;**已显式给 `Authorization` 头则不覆盖**。\n\n## Status Line — `statusLine` 自定义状态栏\n\nClaude Code `statusLine` 对标。REPL **每次提示前**渲染一行用户自定义命令的输出\n(模型 / 分支 / 花费 / 任意你想显示的)。配置在 `.claude/settings.json`:\n\n```jsonc\n{ \"statusLine\": { \"type\": \"command\", \"command\": \"./status.sh\", \"padding\": 0 } }\n// 也接受裸字符串:\"statusLine\": \"./status.sh\"\n```\n\n命令通过 **stdin 收到 JSON 上下文** `{ session_id, model:{id,display_name}, provider,\nworkspace:{current_dir,project_dir}, cwd }`,其 **首行 stdout** 即状态栏。层级 last-wins;\n`false` 可在高层禁用。**best-effort**:命令缺失 / 报错 / 超时(默认 5s)→ 不显示,绝不卡 REPL。\n\n```bash\nchainlesschain statusline preview [--model ] [--json] # 渲染一次预览\nchainlesschain statusline show [--json] # 显示解析后的配置\n```\n\n## Hosted Session API (Phase I)\n\n`cc serve` exposes session-core over WebSocket. Route types (dot-case)\nreturn `.response` envelopes with `{ ok, ... }`:\n\n| Type | Payload fields |\n| ----------------------- | ------------------------------------------------------- |\n| `sessions.list` | `agentId?`, `status?` → `{ sessions[] }` |\n| `sessions.show` | `sessionId` → `{ session, source: \"live\"\\|\"parked\" }` |\n| `sessions.park` | `sessionId` → `{ parked: true }` |\n| `sessions.unpark` | `sessionId` → `{ resumed: true }` |\n| `sessions.end` | `sessionId`, `consolidate?`, `scope?`, `scopeId?`, `agentId?` |\n| `sessions.policy.get` | `sessionId` → `{ policy }` |\n| `sessions.policy.set` | `sessionId`, `policy` (strict/trusted/autopilot) |\n| `memory.store` | `content`, `scope?`, `scopeId?`, `tags?`, `category?` |\n| `memory.recall` | `query?`, `scope?`, `tags?`, `limit?` → `{ results[] }` |\n| `memory.delete` | `id` → `{ deleted: true }` |\n| `memory.consolidate` | `sessionId`, `scope?`, `dryRun?` |\n| `beta.list` / `beta.enable` / `beta.disable` | `flag` (format `-YYYY-MM-DD`) |\n| `usage.session` | `sessionId` → `{ usage }` |\n| `usage.global` | `limit?` → `{ usage: { sessions[], total, byModel[] } }` |\n\nStreaming routes emit intermediate `stream.event` envelopes followed by a\nterminal `.end` envelope:\n\n| Type | Payload fields |\n| ------------ | ----------------------------------------------------------------------------------------------- |\n| `stream.run` | `prompt`, `provider?`, `model?`, `baseUrl?`, `apiKey?` → events `{type:\"stream.event\", event}`, end `{ok, text}` |\n| `sessions.subscribe` | `events?:string[]` (default: all lifecycle) → events `{type:\"stream.event\", event:{type:\"session.\", session}}`, end `{ok, unsubscribed:true, events}` |\n\nLifecycle values forwarded by `sessions.subscribe`: `created`, `adopted`,\n`touched`, `idle`, `parked`, `resumed`, `closed`.\n\nCancel a streaming request by sending `{type:\"cancel\", id}` — the server\ncalls `AbortController.abort()` which detaches listeners (`sessions.subscribe`)\nor aborts the in-flight fetch (`stream.run`).\n"}}, "packages/cli/src/lib/llm-providers.js": {"9e2a3238426499a3de1d228034e66dab91cbfa2c": {"t": 1785746698.8816311, "n": 35231, "h": "362a457cdc6e3bf3", "c": "/**\n * LLM Provider registry — supports multiple AI providers with a unified interface.\n */\n\n/**\n * Built-in provider definitions.\n */\nexport const BUILT_IN_PROVIDERS = {\n ollama: {\n name: \"ollama\",\n displayName: \"Ollama (Local)\",\n baseUrl: \"http://localhost:11434\",\n apiKeyEnv: null,\n models: [\n \"qwen2.5:7b\",\n \"qwen2.5:14b\",\n \"qwen2.5-coder:14b\",\n \"qwen2:7b\",\n \"llama3:8b\",\n \"mistral:7b\",\n \"codellama:7b\",\n ],\n free: true,\n },\n openai: {\n name: \"openai\",\n displayName: \"OpenAI\",\n baseUrl: \"https://api.openai.com/v1\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\", \"o1\"],\n free: false,\n },\n anthropic: {\n name: \"anthropic\",\n displayName: \"Anthropic\",\n baseUrl: \"https://api.anthropic.com/v1\",\n apiKeyEnv: \"ANTHROPIC_API_KEY\",\n models: [\n \"claude-opus-4-6\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5-20251001\",\n ],\n free: false,\n },\n deepseek: {\n name: \"deepseek\",\n displayName: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com/v1\",\n apiKeyEnv: \"DEEPSEEK_API_KEY\",\n models: [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"],\n free: false,\n },\n dashscope: {\n name: \"dashscope\",\n displayName: \"DashScope (Alibaba)\",\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n apiKeyEnv: \"DASHSCOPE_API_KEY\",\n models: [\"qwen-turbo\", \"qwen-plus\", \"qwen-max\"],\n free: false,\n },\n gemini: {\n name: \"gemini\",\n displayName: \"Google Gemini\",\n baseUrl: \"https://generativelanguage.googleapis.com/v1beta\",\n apiKeyEnv: \"GEMINI_API_KEY\",\n models: [\"gemini-2.0-flash\", \"gemini-2.0-pro\", \"gemini-1.5-flash\"],\n free: false,\n },\n mistral: {\n name: \"mistral\",\n displayName: \"Mistral AI\",\n baseUrl: \"https://api.mistral.ai/v1\",\n apiKeyEnv: \"MISTRAL_API_KEY\",\n models: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n ],\n free: false,\n },\n volcengine: {\n name: \"volcengine\",\n displayName: \"Volcengine (火山引擎/豆包)\",\n baseUrl: \"https://ark.cn-beijing.volces.com/api/v3\",\n apiKeyEnv: \"VOLCENGINE_API_KEY\",\n models: [\n \"doubao-seed-2-1-pro-260628\",\n \"doubao-seed-2-0-lite-260215\",\n \"doubao-seed-1-6-251015\",\n \"doubao-seed-1-6-flash-250828\",\n \"doubao-seed-1-6-lite-251015\",\n \"doubao-seed-code\",\n ],\n free: false,\n },\n kimi: {\n name: \"kimi\",\n displayName: \"Kimi (月之暗面)\",\n baseUrl: \"https://api.moonshot.cn/v1\",\n apiKeyEnv: \"MOONSHOT_API_KEY\",\n models: [\n \"moonshot-v1-auto\",\n \"moonshot-v1-8k\",\n \"moonshot-v1-32k\",\n \"moonshot-v1-128k\",\n ],\n free: false,\n },\n minimax: {\n name: \"minimax\",\n displayName: \"MiniMax (海螺AI)\",\n baseUrl: \"https://api.minimax.chat/v1\",\n apiKeyEnv: \"MINIMAX_API_KEY\",\n models: [\"MiniMax-Text-01\", \"abab6.5s-chat\", \"abab5.5-chat\"],\n free: false,\n },\n};\n\n// A connectivity probe (`testProvider`) sends a tiny \"Hi\" and expects a quick\n// reply, so it must fail fast — a probe that hangs forever defeats its purpose\n// (it backs `cc llm test`, provider selection, doctor). Bound it with a total\n// timeout; env-overridable via CC_PROVIDER_TEST_TIMEOUT_MS.\nexport const PROVIDER_TEST_TIMEOUT_MS =\n Number(process.env.CC_PROVIDER_TEST_TIMEOUT_MS) || 20000;\n\nasync function _fetchWithTimeout(\n url,\n opts = {},\n ms = PROVIDER_TEST_TIMEOUT_MS,\n) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n if (timer && typeof timer.unref === \"function\") timer.unref();\n try {\n return await fetch(url, { ...opts, signal: controller.signal });\n } catch (e) {\n if (controller.signal.aborted) {\n throw new Error(`provider test timed out after ${ms / 1000}s`);\n }\n throw e;\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Provider registry — manages available providers and active selection.\n */\nexport class LLMProviderRegistry {\n constructor(db) {\n this.db = db;\n this.providers = new Map();\n this._ensureTable();\n this._loadBuiltins();\n this._loadCustom();\n }\n\n _ensureTable() {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS llm_providers (\n name TEXT PRIMARY KEY,\n display_name TEXT NOT NULL,\n base_url TEXT NOT NULL,\n api_key_env TEXT,\n models TEXT DEFAULT '[]',\n is_active INTEGER DEFAULT 0,\n custom INTEGER DEFAULT 0,\n created_at TEXT DEFAULT (datetime('now'))\n )\n `);\n }\n\n _loadBuiltins() {\n for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {\n this.providers.set(name, { ...def, custom: false });\n }\n }\n\n _loadCustom() {\n const rows = this.db\n .prepare(\"SELECT * FROM llm_providers WHERE custom = 1\")\n .all();\n for (const row of rows) {\n // Tolerate a corrupt `models` TEXT column: `|| \"[]\"` only covers null,\n // not malformed JSON, and an unguarded throw here would abort the whole\n // custom-provider load (and the registry constructor) over one bad row.\n let models = [];\n try {\n const parsed = JSON.parse(row.models || \"[]\");\n if (Array.isArray(parsed)) models = parsed;\n } catch {\n /* corrupt models column — fall back to no models for this provider */\n }\n this.providers.set(row.name, {\n name: row.name,\n displayName: row.display_name,\n baseUrl: row.base_url,\n apiKeyEnv: row.api_key_env,\n models,\n custom: true,\n free: false,\n });\n }\n }\n\n /**\n * List all providers.\n */\n list() {\n const result = [];\n for (const [name, provider] of this.providers) {\n const hasKey = provider.apiKeyEnv\n ? !!process.env[provider.apiKeyEnv]\n : true;\n result.push({\n name,\n displayName: provider.displayName,\n baseUrl: provider.baseUrl,\n models: provider.models,\n hasApiKey: hasKey,\n custom: provider.custom || false,\n free: provider.free || false,\n });\n }\n return result;\n }\n\n /**\n * Get a specific provider.\n */\n get(name) {\n return this.providers.get(name) || null;\n }\n\n /**\n * Add a custom provider.\n */\n addProvider(name, config) {\n const provider = {\n name,\n displayName: config.displayName || name,\n baseUrl: config.baseUrl,\n apiKeyEnv: config.apiKeyEnv || null,\n models: config.models || [],\n custom: true,\n free: config.free || false,\n };\n\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, custom) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n provider.displayName,\n provider.baseUrl,\n provider.apiKeyEnv,\n JSON.stringify(provider.models),\n );\n\n this.providers.set(name, provider);\n return provider;\n }\n\n /**\n * Remove a custom provider.\n */\n removeProvider(name) {\n const provider = this.providers.get(name);\n if (!provider) return false;\n if (!provider.custom)\n throw new Error(`Cannot remove built-in provider \"${name}\"`);\n\n this.db\n .prepare(\"DELETE FROM llm_providers WHERE name = ? AND custom = 1\")\n .run(name);\n this.providers.delete(name);\n return true;\n }\n\n /**\n * Get/set the active provider.\n */\n getActive() {\n const row = this.db\n .prepare(\"SELECT name FROM llm_providers WHERE is_active = 1\")\n .get();\n return row ? row.name : \"ollama\";\n }\n\n setActive(name) {\n if (!this.providers.has(name)) {\n throw new Error(`Provider \"${name}\" not found`);\n }\n // Clear every active flag then mark the new provider active atomically.\n // Without a transaction, a failure on the second statement leaves NO\n // provider active — which getActive() silently masks as the \"ollama\"\n // fallback, so a failed switch would quietly lose the current provider.\n const applyWrites = () => {\n this.db.prepare(\"UPDATE llm_providers SET is_active = 0 WHERE 1=1\").run();\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, is_active) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n this.providers.get(name).displayName,\n this.providers.get(name).baseUrl,\n this.providers.get(name).apiKeyEnv,\n JSON.stringify(this.providers.get(name).models),\n );\n };\n if (this.db && typeof this.db.transaction === \"function\") {\n this.db.transaction(applyWrites)();\n } else {\n applyWrites();\n }\n return this.providers.get(name);\n }\n\n /**\n * Get API key for a provider (from env or config).\n */\n getApiKey(name) {\n const provider = this.providers.get(name);\n if (!provider) return null;\n if (!provider.apiKeyEnv) return null;\n return process.env[provider.apiKeyEnv] || null;\n }\n\n /**\n * Test provider connectivity.\n */\n async testProvider(name, model) {\n const provider = this.providers.get(name);\n if (!provider) throw new Error(`Provider \"${name}\" not found`);\n\n const start = Date.now();\n const testModel = model || provider.models[0];\n\n if (name === \"ollama\") {\n const res = await _fetchWithTimeout(`${provider.baseUrl}/api/generate`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: testModel, prompt: \"Hi\", stream: false }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n return {\n ok: true,\n elapsed: Date.now() - start,\n response: data.response?.trim(),\n };\n }\n\n if (name === \"gemini\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"GEMINI_API_KEY not set\");\n const res = await _fetchWithTimeout(\n `${provider.baseUrl}/models/${testModel}:generateContent?key=${key}`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ contents: [{ parts: [{ text: \"Hi\" }] }] }),\n },\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n if (name === \"anthropic\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"ANTHROPIC_API_KEY not set\");\n const res = await _fetchWithTimeout(`${provider.baseUrl}/messages`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": key,\n \"anthropic-version\": \"2023-06-01\",\n },\n body: JSON.stringify({\n model: testModel,\n max_tokens: 10,\n messages: [{ role: \"user\", content: \"Hi\" }],\n }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.content?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n // OpenAI-compatible (openai, deepseek, dashscope, mistral, volcengine, kimi, minimax)\n const key = this.getApiKey(name);\n if (!key) throw new Error(`${provider.apiKeyEnv} not set`);\n const res = await _fetchWithTimeout(\n `${provider.baseUrl}/chat/completions`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${key}`,\n },\n body: JSON.stringify({\n model: testModel,\n messages: [{ role: \"user\", content: \"Hi\" }],\n max_tokens: 10,\n }),\n },\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n}\n\n/* ─────────────────────────────────────────────────────────────────\n * V2 Governance Layer (in-memory, independent of SQLite llm_providers)\n *\n * Provider profile maturity: pending → active → suspended → retired\n * - retired terminal\n * - suspended → active recovery (cap-exempt)\n *\n * Request lifecycle: queued → running → completed | failed | cancelled\n * - 3 terminals\n * - per-profile pending-request cap counts queued+running\n *\n * Per-owner active-profile cap on pending→active only (recovery exempt).\n * Per-profile pending-request cap enforced at createRequestV2.\n *\n * Auto-flip:\n * - autoSuspendIdleProfilesV2 active w/ lastSeenAt past idle threshold → suspended\n * - autoFailStuckRequestsV2 running w/ startedAt past stuck threshold → failed\n * ───────────────────────────────────────────────────────────────── */\n\nexport const PROVIDER_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n SUSPENDED: \"suspended\",\n RETIRED: \"retired\",\n});\n\nexport const REQUEST_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n RUNNING: \"running\",\n COMPLETED: \"completed\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\n\nconst _PROVIDER_TRANSITIONS_V2 = new Map([\n [\n PROVIDER_MATURITY_V2.PENDING,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.ACTIVE,\n new Set([PROVIDER_MATURITY_V2.SUSPENDED, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.SUSPENDED,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [PROVIDER_MATURITY_V2.RETIRED, new Set()],\n]);\nconst _PROVIDER_TERMINALS_V2 = new Set([PROVIDER_MATURITY_V2.RETIRED]);\n\nconst _REQUEST_TRANSITIONS_V2 = new Map([\n [\n REQUEST_LIFECYCLE_V2.QUEUED,\n new Set([REQUEST_LIFECYCLE_V2.RUNNING, REQUEST_LIFECYCLE_V2.CANCELLED]),\n ],\n [\n REQUEST_LIFECYCLE_V2.RUNNING,\n new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [REQUEST_LIFECYCLE_V2.COMPLETED, new Set()],\n [REQUEST_LIFECYCLE_V2.FAILED, new Set()],\n [REQUEST_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _REQUEST_TERMINALS_V2 = new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n]);\n\nexport const PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER = 8;\nexport const PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE = 16;\nexport const PROVIDER_DEFAULT_PROFILE_IDLE_MS = 14 * 24 * 60 * 60 * 1000;\nexport const PROVIDER_DEFAULT_REQUEST_STUCK_MS = 5 * 60 * 1000;\n\nconst _stateLlmV2 = {\n profiles: new Map(),\n requests: new Map(),\n maxActiveProfilesPerOwner: PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER,\n maxPendingRequestsPerProfile:\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE,\n profileIdleMs: PROVIDER_DEFAULT_PROFILE_IDLE_MS,\n requestStuckMs: PROVIDER_DEFAULT_REQUEST_STUCK_MS,\n};\n\nfunction _posIntLlmV2(n, label) {\n const v = Math.floor(n);\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`${label} must be a positive integer`);\n }\n return v;\n}\n\nfunction _copyProfileV2(p) {\n return { ...p, metadata: { ...p.metadata } };\n}\n\nfunction _copyRequestV2(r) {\n return { ...r, metadata: { ...r.metadata } };\n}\n\nexport function getMaxActiveProfilesPerOwnerV2() {\n return _stateLlmV2.maxActiveProfilesPerOwner;\n}\n\nexport function setMaxActiveProfilesPerOwnerV2(n) {\n _stateLlmV2.maxActiveProfilesPerOwner = _posIntLlmV2(\n n,\n \"maxActiveProfilesPerOwner\",\n );\n}\n\nexport function getMaxPendingRequestsPerProfileV2() {\n return _stateLlmV2.maxPendingRequestsPerProfile;\n}\n\nexport function setMaxPendingRequestsPerProfileV2(n) {\n _stateLlmV2.maxPendingRequestsPerProfile = _posIntLlmV2(\n n,\n \"maxPendingRequestsPerProfile\",\n );\n}\n\nexport function getProfileIdleMsV2() {\n return _stateLlmV2.profileIdleMs;\n}\n\nexport function setProfileIdleMsV2(ms) {\n _stateLlmV2.profileIdleMs = _posIntLlmV2(ms, \"profileIdleMs\");\n}\n\nexport function getRequestStuckMsV2() {\n return _stateLlmV2.requestStuckMs;\n}\n\nexport function setRequestStuckMsV2(ms) {\n _stateLlmV2.requestStuckMs = _posIntLlmV2(ms, \"requestStuckMs\");\n}\n\nexport function getActiveProfileCountV2(ownerId) {\n let count = 0;\n for (const p of _stateLlmV2.profiles.values()) {\n if (p.ownerId === ownerId && p.status === PROVIDER_MATURITY_V2.ACTIVE) {\n count++;\n }\n }\n return count;\n}\n\nexport function getPendingRequestCountV2(profileId) {\n let count = 0;\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.profileId === profileId &&\n (r.status === REQUEST_LIFECYCLE_V2.QUEUED ||\n r.status === REQUEST_LIFECYCLE_V2.RUNNING)\n ) {\n count++;\n }\n }\n return count;\n}\n\nexport function registerProfileV2(\n id,\n { ownerId, provider, model, metadata } = {},\n) {\n if (!id) throw new Error(\"profile id is required\");\n if (!ownerId) throw new Error(\"ownerId is required\");\n if (!provider) throw new Error(\"provider is required\");\n if (_stateLlmV2.profiles.has(id))\n throw new Error(`profile ${id} already exists`);\n const now = Date.now();\n const profile = {\n id,\n ownerId,\n provider,\n model: model || \"default\",\n status: PROVIDER_MATURITY_V2.PENDING,\n createdAt: now,\n lastSeenAt: now,\n activatedAt: null,\n retiredAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.profiles.set(id, profile);\n return _copyProfileV2(profile);\n}\n\nexport function getProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n return p ? _copyProfileV2(p) : null;\n}\n\nexport function listProfilesV2({ ownerId, status, provider } = {}) {\n const out = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (ownerId && p.ownerId !== ownerId) continue;\n if (status && p.status !== status) continue;\n if (provider && p.provider !== provider) continue;\n out.push(_copyProfileV2(p));\n }\n return out;\n}\n\nexport function setProfileStatusV2(id, next) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n const allowed = _PROVIDER_TRANSITIONS_V2.get(p.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid profile transition: ${p.status} → ${next}`);\n }\n if (\n p.status === PROVIDER_MATURITY_V2.PENDING &&\n next === PROVIDER_MATURITY_V2.ACTIVE\n ) {\n const count = getActiveProfileCountV2(p.ownerId);\n if (count >= _stateLlmV2.maxActiveProfilesPerOwner) {\n throw new Error(\n `owner ${p.ownerId} active-profile cap reached (${count}/${_stateLlmV2.maxActiveProfilesPerOwner})`,\n );\n }\n }\n const now = Date.now();\n p.status = next;\n p.lastSeenAt = now;\n if (next === PROVIDER_MATURITY_V2.ACTIVE && !p.activatedAt)\n p.activatedAt = now;\n if (_PROVIDER_TERMINALS_V2.has(next) && !p.retiredAt) p.retiredAt = now;\n return _copyProfileV2(p);\n}\n\nexport function activateProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.ACTIVE);\n}\n\nexport function suspendProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.SUSPENDED);\n}\n\nexport function retireProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.RETIRED);\n}\n\nexport function touchProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n p.lastSeenAt = Date.now();\n return _copyProfileV2(p);\n}\n\nexport function createRequestV2(id, { profileId, kind, metadata } = {}) {\n if (!id) throw new Error(\"request id is required\");\n if (!profileId) throw new Error(\"profileId is required\");\n if (_stateLlmV2.requests.has(id))\n throw new Error(`request ${id} already exists`);\n const profile = _stateLlmV2.profiles.get(profileId);\n if (!profile) throw new Error(`profile ${profileId} not found`);\n const pending = getPendingRequestCountV2(profileId);\n if (pending >= _stateLlmV2.maxPendingRequestsPerProfile) {\n throw new Error(\n `profile ${profileId} pending-request cap reached (${pending}/${_stateLlmV2.maxPendingRequestsPerProfile})`,\n );\n }\n const now = Date.now();\n const req = {\n id,\n profileId,\n kind: kind || \"completion\",\n status: REQUEST_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n lastSeenAt: now,\n startedAt: null,\n settledAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.requests.set(id, req);\n return _copyRequestV2(req);\n}\n\nexport function getRequestV2(id) {\n const r = _stateLlmV2.requests.get(id);\n return r ? _copyRequestV2(r) : null;\n}\n\nexport function listRequestsV2({ profileId, status } = {}) {\n const out = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (profileId && r.profileId !== profileId) continue;\n if (status && r.status !== status) continue;\n out.push(_copyRequestV2(r));\n }\n return out;\n}\n\nexport function setRequestStatusV2(id, next) {\n const r = _stateLlmV2.requests.get(id);\n if (!r) throw new Error(`request ${id} not found`);\n const allowed = _REQUEST_TRANSITIONS_V2.get(r.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid request transition: ${r.status} → ${next}`);\n }\n const now = Date.now();\n r.status = next;\n r.lastSeenAt = now;\n if (next === REQUEST_LIFECYCLE_V2.RUNNING && !r.startedAt) r.startedAt = now;\n if (_REQUEST_TERMINALS_V2.has(next) && !r.settledAt) r.settledAt = now;\n return _copyRequestV2(r);\n}\n\nexport function startRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.RUNNING);\n}\n\nexport function completeRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.COMPLETED);\n}\n\nexport function failRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.FAILED);\n}\n\nexport function cancelRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.CANCELLED);\n}\n\nexport function autoSuspendIdleProfilesV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (\n p.status === PROVIDER_MATURITY_V2.ACTIVE &&\n now - p.lastSeenAt >= _stateLlmV2.profileIdleMs\n ) {\n p.status = PROVIDER_MATURITY_V2.SUSPENDED;\n p.lastSeenAt = now;\n flipped.push(p.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function autoFailStuckRequestsV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.status === REQUEST_LIFECYCLE_V2.RUNNING &&\n r.startedAt &&\n now - r.startedAt >= _stateLlmV2.requestStuckMs\n ) {\n r.status = REQUEST_LIFECYCLE_V2.FAILED;\n r.lastSeenAt = now;\n if (!r.settledAt) r.settledAt = now;\n flipped.push(r.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function getLlmProvidersStatsV2() {\n const profilesByStatus = {};\n for (const s of Object.values(PROVIDER_MATURITY_V2)) profilesByStatus[s] = 0;\n for (const p of _stateLlmV2.profiles.values()) profilesByStatus[p.status]++;\n const requestsByStatus = {};\n for (const s of Object.values(REQUEST_LIFECYCLE_V2)) requestsByStatus[s] = 0;\n for (const r of _stateLlmV2.requests.values()) requestsByStatus[r.status]++;\n return {\n totalProfilesV2: _stateLlmV2.profiles.size,\n totalRequestsV2: _stateLlmV2.requests.size,\n maxActiveProfilesPerOwner: _stateLlmV2.maxActiveProfilesPerOwner,\n maxPendingRequestsPerProfile: _stateLlmV2.maxPendingRequestsPerProfile,\n profileIdleMs: _stateLlmV2.profileIdleMs,\n requestStuckMs: _stateLlmV2.requestStuckMs,\n profilesByStatus,\n requestsByStatus,\n };\n}\n\nexport function _resetStateLlmProvidersV2() {\n _stateLlmV2.profiles.clear();\n _stateLlmV2.requests.clear();\n _stateLlmV2.maxActiveProfilesPerOwner = PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER;\n _stateLlmV2.maxPendingRequestsPerProfile =\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE;\n _stateLlmV2.profileIdleMs = PROVIDER_DEFAULT_PROFILE_IDLE_MS;\n _stateLlmV2.requestStuckMs = PROVIDER_DEFAULT_REQUEST_STUCK_MS;\n}\n\n// =====================================================================\n// llm-providers V2 governance overlay (iter22)\n// =====================================================================\nexport const LLMGOV_PROFILE_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n DEGRADED: \"degraded\",\n ARCHIVED: \"archived\",\n});\nexport const LLMGOV_COMPLETION_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n INFERRING: \"inferring\",\n INFERRED: \"inferred\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\nconst _llmgovPTrans = new Map([\n [\n LLMGOV_PROFILE_MATURITY_V2.PENDING,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [LLMGOV_PROFILE_MATURITY_V2.ARCHIVED, new Set()],\n]);\nconst _llmgovPTerminal = new Set([LLMGOV_PROFILE_MATURITY_V2.ARCHIVED]);\nconst _llmgovJTrans = new Map([\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _llmgovPsV2 = new Map();\nconst _llmgovJsV2 = new Map();\nlet _llmgovMaxActive = 8,\n _llmgovMaxPending = 25,\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000,\n _llmgovStuckMs = 60 * 1000;\nfunction _llmgovPos(n, label) {\n const v = Math.floor(Number(n));\n if (!Number.isFinite(v) || v <= 0)\n throw new Error(`${label} must be positive integer`);\n return v;\n}\nfunction _llmgovCheckP(from, to) {\n const a = _llmgovPTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov profile transition ${from} → ${to}`);\n}\nfunction _llmgovCheckJ(from, to) {\n const a = _llmgovJTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov completion transition ${from} → ${to}`);\n}\nfunction _llmgovCountActive(owner) {\n let c = 0;\n for (const p of _llmgovPsV2.values())\n if (p.owner === owner && p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE)\n c++;\n return c;\n}\nfunction _llmgovCountPending(profileId) {\n let c = 0;\n for (const j of _llmgovJsV2.values())\n if (\n j.profileId === profileId &&\n (j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED ||\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING)\n )\n c++;\n return c;\n}\nexport function setMaxActiveLlmgovProfilesPerOwnerV2(n) {\n _llmgovMaxActive = _llmgovPos(n, \"maxActiveLlmgovProfilesPerOwner\");\n}\nexport function getMaxActiveLlmgovProfilesPerOwnerV2() {\n return _llmgovMaxActive;\n}\nexport function setMaxPendingLlmgovCompletionsPerProfileV2(n) {\n _llmgovMaxPending = _llmgovPos(n, \"maxPendingLlmgovCompletionsPerProfile\");\n}\nexport function getMaxPendingLlmgovCompletionsPerProfileV2() {\n return _llmgovMaxPending;\n}\nexport function setLlmgovProfileIdleMsV2(n) {\n _llmgovIdleMs = _llmgovPos(n, \"llmgovProfileIdleMs\");\n}\nexport function getLlmgovProfileIdleMsV2() {\n return _llmgovIdleMs;\n}\nexport function setLlmgovCompletionStuckMsV2(n) {\n _llmgovStuckMs = _llmgovPos(n, \"llmgovCompletionStuckMs\");\n}\nexport function getLlmgovCompletionStuckMsV2() {\n return _llmgovStuckMs;\n}\nexport function _resetStateLlmProvidersGovV2() {\n _llmgovPsV2.clear();\n _llmgovJsV2.clear();\n _llmgovMaxActive = 8;\n _llmgovMaxPending = 25;\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000;\n _llmgovStuckMs = 60 * 1000;\n}\nexport function registerLlmgovProfileV2({\n id,\n owner,\n provider,\n metadata,\n} = {}) {\n if (!id || !owner) throw new Error(\"id and owner required\");\n if (_llmgovPsV2.has(id))\n throw new Error(`llmgov profile ${id} already exists`);\n const now = Date.now();\n const p = {\n id,\n owner,\n provider: provider || \"ollama\",\n status: LLMGOV_PROFILE_MATURITY_V2.PENDING,\n createdAt: now,\n updatedAt: now,\n lastTouchedAt: now,\n activatedAt: null,\n archivedAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovPsV2.set(id, p);\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function activateLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n const isInitial = p.status === LLMGOV_PROFILE_MATURITY_V2.PENDING;\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ACTIVE);\n if (isInitial && _llmgovCountActive(p.owner) >= _llmgovMaxActive)\n throw new Error(`max active llmgov profiles for owner ${p.owner} reached`);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ACTIVE;\n p.updatedAt = now;\n p.lastTouchedAt = now;\n if (!p.activatedAt) p.activatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function degradeLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.DEGRADED);\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = Date.now();\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function archiveLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ARCHIVED);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ARCHIVED;\n p.updatedAt = now;\n if (!p.archivedAt) p.archivedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function touchLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n if (_llmgovPTerminal.has(p.status))\n throw new Error(`cannot touch terminal llmgov profile ${id}`);\n const now = Date.now();\n p.lastTouchedAt = now;\n p.updatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function getLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) return null;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function listLlmgovProfilesV2() {\n return [..._llmgovPsV2.values()].map((p) => ({\n ...p,\n metadata: { ...p.metadata },\n }));\n}\nexport function createLlmgovCompletionV2({\n id,\n profileId,\n model,\n metadata,\n} = {}) {\n if (!id || !profileId) throw new Error(\"id and profileId required\");\n if (_llmgovJsV2.has(id))\n throw new Error(`llmgov completion ${id} already exists`);\n if (!_llmgovPsV2.has(profileId))\n throw new Error(`llmgov profile ${profileId} not found`);\n if (_llmgovCountPending(profileId) >= _llmgovMaxPending)\n throw new Error(\n `max pending llmgov completions for profile ${profileId} reached`,\n );\n const now = Date.now();\n const j = {\n id,\n profileId,\n model: model || \"\",\n status: LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n updatedAt: now,\n startedAt: null,\n settledAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovJsV2.set(id, j);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function inferringLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING;\n j.updatedAt = now;\n if (!j.startedAt) j.startedAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function completeCompletionLlmgovV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function failLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.failReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function cancelLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.cancelReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function getLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) return null;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function listLlmgovCompletionsV2() {\n return [..._llmgovJsV2.values()].map((j) => ({\n ...j,\n metadata: { ...j.metadata },\n }));\n}\nexport function autoDegradeIdleLlmgovProfilesV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const p of _llmgovPsV2.values())\n if (\n p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE &&\n t - p.lastTouchedAt >= _llmgovIdleMs\n ) {\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = t;\n flipped.push(p.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function autoFailStuckLlmgovCompletionsV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const j of _llmgovJsV2.values())\n if (\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING &&\n j.startedAt != null &&\n t - j.startedAt >= _llmgovStuckMs\n ) {\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = t;\n if (!j.settledAt) j.settledAt = t;\n j.metadata.failReason = \"auto-fail-stuck\";\n flipped.push(j.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function getLlmProvidersGovStatsV2() {\n const profilesByStatus = {};\n for (const v of Object.values(LLMGOV_PROFILE_MATURITY_V2))\n profilesByStatus[v] = 0;\n for (const p of _llmgovPsV2.values()) profilesByStatus[p.status]++;\n const completionsByStatus = {};\n for (const v of Object.values(LLMGOV_COMPLETION_LIFECYCLE_V2))\n completionsByStatus[v] = 0;\n for (const j of _llmgovJsV2.values()) completionsByStatus[j.status]++;\n return {\n totalLlmgovProfilesV2: _llmgovPsV2.size,\n totalLlmgovCompletionsV2: _llmgovJsV2.size,\n maxActiveLlmgovProfilesPerOwner: _llmgovMaxActive,\n maxPendingLlmgovCompletionsPerProfile: _llmgovMaxPending,\n llmgovProfileIdleMs: _llmgovIdleMs,\n llmgovCompletionStuckMs: _llmgovStuckMs,\n profilesByStatus,\n completionsByStatus,\n };\n}\n"}, "49e96857b0d08f7211f788cadec6edd84d0ab43a": {"t": 1785754069.6456494, "n": 35231, "h": "362a457cdc6e3bf3", "c": "/**\n * LLM Provider registry — supports multiple AI providers with a unified interface.\n */\n\n/**\n * Built-in provider definitions.\n */\nexport const BUILT_IN_PROVIDERS = {\n ollama: {\n name: \"ollama\",\n displayName: \"Ollama (Local)\",\n baseUrl: \"http://localhost:11434\",\n apiKeyEnv: null,\n models: [\n \"qwen2.5:7b\",\n \"qwen2.5:14b\",\n \"qwen2.5-coder:14b\",\n \"qwen2:7b\",\n \"llama3:8b\",\n \"mistral:7b\",\n \"codellama:7b\",\n ],\n free: true,\n },\n openai: {\n name: \"openai\",\n displayName: \"OpenAI\",\n baseUrl: \"https://api.openai.com/v1\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\", \"o1\"],\n free: false,\n },\n anthropic: {\n name: \"anthropic\",\n displayName: \"Anthropic\",\n baseUrl: \"https://api.anthropic.com/v1\",\n apiKeyEnv: \"ANTHROPIC_API_KEY\",\n models: [\n \"claude-opus-4-6\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5-20251001\",\n ],\n free: false,\n },\n deepseek: {\n name: \"deepseek\",\n displayName: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com/v1\",\n apiKeyEnv: \"DEEPSEEK_API_KEY\",\n models: [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"],\n free: false,\n },\n dashscope: {\n name: \"dashscope\",\n displayName: \"DashScope (Alibaba)\",\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n apiKeyEnv: \"DASHSCOPE_API_KEY\",\n models: [\"qwen-turbo\", \"qwen-plus\", \"qwen-max\"],\n free: false,\n },\n gemini: {\n name: \"gemini\",\n displayName: \"Google Gemini\",\n baseUrl: \"https://generativelanguage.googleapis.com/v1beta\",\n apiKeyEnv: \"GEMINI_API_KEY\",\n models: [\"gemini-2.0-flash\", \"gemini-2.0-pro\", \"gemini-1.5-flash\"],\n free: false,\n },\n mistral: {\n name: \"mistral\",\n displayName: \"Mistral AI\",\n baseUrl: \"https://api.mistral.ai/v1\",\n apiKeyEnv: \"MISTRAL_API_KEY\",\n models: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n ],\n free: false,\n },\n volcengine: {\n name: \"volcengine\",\n displayName: \"Volcengine (火山引擎/豆包)\",\n baseUrl: \"https://ark.cn-beijing.volces.com/api/v3\",\n apiKeyEnv: \"VOLCENGINE_API_KEY\",\n models: [\n \"doubao-seed-2-1-pro-260628\",\n \"doubao-seed-2-0-lite-260215\",\n \"doubao-seed-1-6-251015\",\n \"doubao-seed-1-6-flash-250828\",\n \"doubao-seed-1-6-lite-251015\",\n \"doubao-seed-code\",\n ],\n free: false,\n },\n kimi: {\n name: \"kimi\",\n displayName: \"Kimi (月之暗面)\",\n baseUrl: \"https://api.moonshot.cn/v1\",\n apiKeyEnv: \"MOONSHOT_API_KEY\",\n models: [\n \"moonshot-v1-auto\",\n \"moonshot-v1-8k\",\n \"moonshot-v1-32k\",\n \"moonshot-v1-128k\",\n ],\n free: false,\n },\n minimax: {\n name: \"minimax\",\n displayName: \"MiniMax (海螺AI)\",\n baseUrl: \"https://api.minimax.chat/v1\",\n apiKeyEnv: \"MINIMAX_API_KEY\",\n models: [\"MiniMax-Text-01\", \"abab6.5s-chat\", \"abab5.5-chat\"],\n free: false,\n },\n};\n\n// A connectivity probe (`testProvider`) sends a tiny \"Hi\" and expects a quick\n// reply, so it must fail fast — a probe that hangs forever defeats its purpose\n// (it backs `cc llm test`, provider selection, doctor). Bound it with a total\n// timeout; env-overridable via CC_PROVIDER_TEST_TIMEOUT_MS.\nexport const PROVIDER_TEST_TIMEOUT_MS =\n Number(process.env.CC_PROVIDER_TEST_TIMEOUT_MS) || 20000;\n\nasync function _fetchWithTimeout(\n url,\n opts = {},\n ms = PROVIDER_TEST_TIMEOUT_MS,\n) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n if (timer && typeof timer.unref === \"function\") timer.unref();\n try {\n return await fetch(url, { ...opts, signal: controller.signal });\n } catch (e) {\n if (controller.signal.aborted) {\n throw new Error(`provider test timed out after ${ms / 1000}s`);\n }\n throw e;\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Provider registry — manages available providers and active selection.\n */\nexport class LLMProviderRegistry {\n constructor(db) {\n this.db = db;\n this.providers = new Map();\n this._ensureTable();\n this._loadBuiltins();\n this._loadCustom();\n }\n\n _ensureTable() {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS llm_providers (\n name TEXT PRIMARY KEY,\n display_name TEXT NOT NULL,\n base_url TEXT NOT NULL,\n api_key_env TEXT,\n models TEXT DEFAULT '[]',\n is_active INTEGER DEFAULT 0,\n custom INTEGER DEFAULT 0,\n created_at TEXT DEFAULT (datetime('now'))\n )\n `);\n }\n\n _loadBuiltins() {\n for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {\n this.providers.set(name, { ...def, custom: false });\n }\n }\n\n _loadCustom() {\n const rows = this.db\n .prepare(\"SELECT * FROM llm_providers WHERE custom = 1\")\n .all();\n for (const row of rows) {\n // Tolerate a corrupt `models` TEXT column: `|| \"[]\"` only covers null,\n // not malformed JSON, and an unguarded throw here would abort the whole\n // custom-provider load (and the registry constructor) over one bad row.\n let models = [];\n try {\n const parsed = JSON.parse(row.models || \"[]\");\n if (Array.isArray(parsed)) models = parsed;\n } catch {\n /* corrupt models column — fall back to no models for this provider */\n }\n this.providers.set(row.name, {\n name: row.name,\n displayName: row.display_name,\n baseUrl: row.base_url,\n apiKeyEnv: row.api_key_env,\n models,\n custom: true,\n free: false,\n });\n }\n }\n\n /**\n * List all providers.\n */\n list() {\n const result = [];\n for (const [name, provider] of this.providers) {\n const hasKey = provider.apiKeyEnv\n ? !!process.env[provider.apiKeyEnv]\n : true;\n result.push({\n name,\n displayName: provider.displayName,\n baseUrl: provider.baseUrl,\n models: provider.models,\n hasApiKey: hasKey,\n custom: provider.custom || false,\n free: provider.free || false,\n });\n }\n return result;\n }\n\n /**\n * Get a specific provider.\n */\n get(name) {\n return this.providers.get(name) || null;\n }\n\n /**\n * Add a custom provider.\n */\n addProvider(name, config) {\n const provider = {\n name,\n displayName: config.displayName || name,\n baseUrl: config.baseUrl,\n apiKeyEnv: config.apiKeyEnv || null,\n models: config.models || [],\n custom: true,\n free: config.free || false,\n };\n\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, custom) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n provider.displayName,\n provider.baseUrl,\n provider.apiKeyEnv,\n JSON.stringify(provider.models),\n );\n\n this.providers.set(name, provider);\n return provider;\n }\n\n /**\n * Remove a custom provider.\n */\n removeProvider(name) {\n const provider = this.providers.get(name);\n if (!provider) return false;\n if (!provider.custom)\n throw new Error(`Cannot remove built-in provider \"${name}\"`);\n\n this.db\n .prepare(\"DELETE FROM llm_providers WHERE name = ? AND custom = 1\")\n .run(name);\n this.providers.delete(name);\n return true;\n }\n\n /**\n * Get/set the active provider.\n */\n getActive() {\n const row = this.db\n .prepare(\"SELECT name FROM llm_providers WHERE is_active = 1\")\n .get();\n return row ? row.name : \"ollama\";\n }\n\n setActive(name) {\n if (!this.providers.has(name)) {\n throw new Error(`Provider \"${name}\" not found`);\n }\n // Clear every active flag then mark the new provider active atomically.\n // Without a transaction, a failure on the second statement leaves NO\n // provider active — which getActive() silently masks as the \"ollama\"\n // fallback, so a failed switch would quietly lose the current provider.\n const applyWrites = () => {\n this.db.prepare(\"UPDATE llm_providers SET is_active = 0 WHERE 1=1\").run();\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, is_active) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n this.providers.get(name).displayName,\n this.providers.get(name).baseUrl,\n this.providers.get(name).apiKeyEnv,\n JSON.stringify(this.providers.get(name).models),\n );\n };\n if (this.db && typeof this.db.transaction === \"function\") {\n this.db.transaction(applyWrites)();\n } else {\n applyWrites();\n }\n return this.providers.get(name);\n }\n\n /**\n * Get API key for a provider (from env or config).\n */\n getApiKey(name) {\n const provider = this.providers.get(name);\n if (!provider) return null;\n if (!provider.apiKeyEnv) return null;\n return process.env[provider.apiKeyEnv] || null;\n }\n\n /**\n * Test provider connectivity.\n */\n async testProvider(name, model) {\n const provider = this.providers.get(name);\n if (!provider) throw new Error(`Provider \"${name}\" not found`);\n\n const start = Date.now();\n const testModel = model || provider.models[0];\n\n if (name === \"ollama\") {\n const res = await _fetchWithTimeout(`${provider.baseUrl}/api/generate`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: testModel, prompt: \"Hi\", stream: false }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n return {\n ok: true,\n elapsed: Date.now() - start,\n response: data.response?.trim(),\n };\n }\n\n if (name === \"gemini\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"GEMINI_API_KEY not set\");\n const res = await _fetchWithTimeout(\n `${provider.baseUrl}/models/${testModel}:generateContent?key=${key}`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ contents: [{ parts: [{ text: \"Hi\" }] }] }),\n },\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n if (name === \"anthropic\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"ANTHROPIC_API_KEY not set\");\n const res = await _fetchWithTimeout(`${provider.baseUrl}/messages`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": key,\n \"anthropic-version\": \"2023-06-01\",\n },\n body: JSON.stringify({\n model: testModel,\n max_tokens: 10,\n messages: [{ role: \"user\", content: \"Hi\" }],\n }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.content?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n // OpenAI-compatible (openai, deepseek, dashscope, mistral, volcengine, kimi, minimax)\n const key = this.getApiKey(name);\n if (!key) throw new Error(`${provider.apiKeyEnv} not set`);\n const res = await _fetchWithTimeout(\n `${provider.baseUrl}/chat/completions`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${key}`,\n },\n body: JSON.stringify({\n model: testModel,\n messages: [{ role: \"user\", content: \"Hi\" }],\n max_tokens: 10,\n }),\n },\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n}\n\n/* ─────────────────────────────────────────────────────────────────\n * V2 Governance Layer (in-memory, independent of SQLite llm_providers)\n *\n * Provider profile maturity: pending → active → suspended → retired\n * - retired terminal\n * - suspended → active recovery (cap-exempt)\n *\n * Request lifecycle: queued → running → completed | failed | cancelled\n * - 3 terminals\n * - per-profile pending-request cap counts queued+running\n *\n * Per-owner active-profile cap on pending→active only (recovery exempt).\n * Per-profile pending-request cap enforced at createRequestV2.\n *\n * Auto-flip:\n * - autoSuspendIdleProfilesV2 active w/ lastSeenAt past idle threshold → suspended\n * - autoFailStuckRequestsV2 running w/ startedAt past stuck threshold → failed\n * ───────────────────────────────────────────────────────────────── */\n\nexport const PROVIDER_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n SUSPENDED: \"suspended\",\n RETIRED: \"retired\",\n});\n\nexport const REQUEST_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n RUNNING: \"running\",\n COMPLETED: \"completed\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\n\nconst _PROVIDER_TRANSITIONS_V2 = new Map([\n [\n PROVIDER_MATURITY_V2.PENDING,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.ACTIVE,\n new Set([PROVIDER_MATURITY_V2.SUSPENDED, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.SUSPENDED,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [PROVIDER_MATURITY_V2.RETIRED, new Set()],\n]);\nconst _PROVIDER_TERMINALS_V2 = new Set([PROVIDER_MATURITY_V2.RETIRED]);\n\nconst _REQUEST_TRANSITIONS_V2 = new Map([\n [\n REQUEST_LIFECYCLE_V2.QUEUED,\n new Set([REQUEST_LIFECYCLE_V2.RUNNING, REQUEST_LIFECYCLE_V2.CANCELLED]),\n ],\n [\n REQUEST_LIFECYCLE_V2.RUNNING,\n new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [REQUEST_LIFECYCLE_V2.COMPLETED, new Set()],\n [REQUEST_LIFECYCLE_V2.FAILED, new Set()],\n [REQUEST_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _REQUEST_TERMINALS_V2 = new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n]);\n\nexport const PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER = 8;\nexport const PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE = 16;\nexport const PROVIDER_DEFAULT_PROFILE_IDLE_MS = 14 * 24 * 60 * 60 * 1000;\nexport const PROVIDER_DEFAULT_REQUEST_STUCK_MS = 5 * 60 * 1000;\n\nconst _stateLlmV2 = {\n profiles: new Map(),\n requests: new Map(),\n maxActiveProfilesPerOwner: PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER,\n maxPendingRequestsPerProfile:\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE,\n profileIdleMs: PROVIDER_DEFAULT_PROFILE_IDLE_MS,\n requestStuckMs: PROVIDER_DEFAULT_REQUEST_STUCK_MS,\n};\n\nfunction _posIntLlmV2(n, label) {\n const v = Math.floor(n);\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`${label} must be a positive integer`);\n }\n return v;\n}\n\nfunction _copyProfileV2(p) {\n return { ...p, metadata: { ...p.metadata } };\n}\n\nfunction _copyRequestV2(r) {\n return { ...r, metadata: { ...r.metadata } };\n}\n\nexport function getMaxActiveProfilesPerOwnerV2() {\n return _stateLlmV2.maxActiveProfilesPerOwner;\n}\n\nexport function setMaxActiveProfilesPerOwnerV2(n) {\n _stateLlmV2.maxActiveProfilesPerOwner = _posIntLlmV2(\n n,\n \"maxActiveProfilesPerOwner\",\n );\n}\n\nexport function getMaxPendingRequestsPerProfileV2() {\n return _stateLlmV2.maxPendingRequestsPerProfile;\n}\n\nexport function setMaxPendingRequestsPerProfileV2(n) {\n _stateLlmV2.maxPendingRequestsPerProfile = _posIntLlmV2(\n n,\n \"maxPendingRequestsPerProfile\",\n );\n}\n\nexport function getProfileIdleMsV2() {\n return _stateLlmV2.profileIdleMs;\n}\n\nexport function setProfileIdleMsV2(ms) {\n _stateLlmV2.profileIdleMs = _posIntLlmV2(ms, \"profileIdleMs\");\n}\n\nexport function getRequestStuckMsV2() {\n return _stateLlmV2.requestStuckMs;\n}\n\nexport function setRequestStuckMsV2(ms) {\n _stateLlmV2.requestStuckMs = _posIntLlmV2(ms, \"requestStuckMs\");\n}\n\nexport function getActiveProfileCountV2(ownerId) {\n let count = 0;\n for (const p of _stateLlmV2.profiles.values()) {\n if (p.ownerId === ownerId && p.status === PROVIDER_MATURITY_V2.ACTIVE) {\n count++;\n }\n }\n return count;\n}\n\nexport function getPendingRequestCountV2(profileId) {\n let count = 0;\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.profileId === profileId &&\n (r.status === REQUEST_LIFECYCLE_V2.QUEUED ||\n r.status === REQUEST_LIFECYCLE_V2.RUNNING)\n ) {\n count++;\n }\n }\n return count;\n}\n\nexport function registerProfileV2(\n id,\n { ownerId, provider, model, metadata } = {},\n) {\n if (!id) throw new Error(\"profile id is required\");\n if (!ownerId) throw new Error(\"ownerId is required\");\n if (!provider) throw new Error(\"provider is required\");\n if (_stateLlmV2.profiles.has(id))\n throw new Error(`profile ${id} already exists`);\n const now = Date.now();\n const profile = {\n id,\n ownerId,\n provider,\n model: model || \"default\",\n status: PROVIDER_MATURITY_V2.PENDING,\n createdAt: now,\n lastSeenAt: now,\n activatedAt: null,\n retiredAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.profiles.set(id, profile);\n return _copyProfileV2(profile);\n}\n\nexport function getProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n return p ? _copyProfileV2(p) : null;\n}\n\nexport function listProfilesV2({ ownerId, status, provider } = {}) {\n const out = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (ownerId && p.ownerId !== ownerId) continue;\n if (status && p.status !== status) continue;\n if (provider && p.provider !== provider) continue;\n out.push(_copyProfileV2(p));\n }\n return out;\n}\n\nexport function setProfileStatusV2(id, next) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n const allowed = _PROVIDER_TRANSITIONS_V2.get(p.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid profile transition: ${p.status} → ${next}`);\n }\n if (\n p.status === PROVIDER_MATURITY_V2.PENDING &&\n next === PROVIDER_MATURITY_V2.ACTIVE\n ) {\n const count = getActiveProfileCountV2(p.ownerId);\n if (count >= _stateLlmV2.maxActiveProfilesPerOwner) {\n throw new Error(\n `owner ${p.ownerId} active-profile cap reached (${count}/${_stateLlmV2.maxActiveProfilesPerOwner})`,\n );\n }\n }\n const now = Date.now();\n p.status = next;\n p.lastSeenAt = now;\n if (next === PROVIDER_MATURITY_V2.ACTIVE && !p.activatedAt)\n p.activatedAt = now;\n if (_PROVIDER_TERMINALS_V2.has(next) && !p.retiredAt) p.retiredAt = now;\n return _copyProfileV2(p);\n}\n\nexport function activateProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.ACTIVE);\n}\n\nexport function suspendProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.SUSPENDED);\n}\n\nexport function retireProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.RETIRED);\n}\n\nexport function touchProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n p.lastSeenAt = Date.now();\n return _copyProfileV2(p);\n}\n\nexport function createRequestV2(id, { profileId, kind, metadata } = {}) {\n if (!id) throw new Error(\"request id is required\");\n if (!profileId) throw new Error(\"profileId is required\");\n if (_stateLlmV2.requests.has(id))\n throw new Error(`request ${id} already exists`);\n const profile = _stateLlmV2.profiles.get(profileId);\n if (!profile) throw new Error(`profile ${profileId} not found`);\n const pending = getPendingRequestCountV2(profileId);\n if (pending >= _stateLlmV2.maxPendingRequestsPerProfile) {\n throw new Error(\n `profile ${profileId} pending-request cap reached (${pending}/${_stateLlmV2.maxPendingRequestsPerProfile})`,\n );\n }\n const now = Date.now();\n const req = {\n id,\n profileId,\n kind: kind || \"completion\",\n status: REQUEST_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n lastSeenAt: now,\n startedAt: null,\n settledAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.requests.set(id, req);\n return _copyRequestV2(req);\n}\n\nexport function getRequestV2(id) {\n const r = _stateLlmV2.requests.get(id);\n return r ? _copyRequestV2(r) : null;\n}\n\nexport function listRequestsV2({ profileId, status } = {}) {\n const out = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (profileId && r.profileId !== profileId) continue;\n if (status && r.status !== status) continue;\n out.push(_copyRequestV2(r));\n }\n return out;\n}\n\nexport function setRequestStatusV2(id, next) {\n const r = _stateLlmV2.requests.get(id);\n if (!r) throw new Error(`request ${id} not found`);\n const allowed = _REQUEST_TRANSITIONS_V2.get(r.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid request transition: ${r.status} → ${next}`);\n }\n const now = Date.now();\n r.status = next;\n r.lastSeenAt = now;\n if (next === REQUEST_LIFECYCLE_V2.RUNNING && !r.startedAt) r.startedAt = now;\n if (_REQUEST_TERMINALS_V2.has(next) && !r.settledAt) r.settledAt = now;\n return _copyRequestV2(r);\n}\n\nexport function startRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.RUNNING);\n}\n\nexport function completeRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.COMPLETED);\n}\n\nexport function failRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.FAILED);\n}\n\nexport function cancelRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.CANCELLED);\n}\n\nexport function autoSuspendIdleProfilesV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (\n p.status === PROVIDER_MATURITY_V2.ACTIVE &&\n now - p.lastSeenAt >= _stateLlmV2.profileIdleMs\n ) {\n p.status = PROVIDER_MATURITY_V2.SUSPENDED;\n p.lastSeenAt = now;\n flipped.push(p.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function autoFailStuckRequestsV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.status === REQUEST_LIFECYCLE_V2.RUNNING &&\n r.startedAt &&\n now - r.startedAt >= _stateLlmV2.requestStuckMs\n ) {\n r.status = REQUEST_LIFECYCLE_V2.FAILED;\n r.lastSeenAt = now;\n if (!r.settledAt) r.settledAt = now;\n flipped.push(r.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function getLlmProvidersStatsV2() {\n const profilesByStatus = {};\n for (const s of Object.values(PROVIDER_MATURITY_V2)) profilesByStatus[s] = 0;\n for (const p of _stateLlmV2.profiles.values()) profilesByStatus[p.status]++;\n const requestsByStatus = {};\n for (const s of Object.values(REQUEST_LIFECYCLE_V2)) requestsByStatus[s] = 0;\n for (const r of _stateLlmV2.requests.values()) requestsByStatus[r.status]++;\n return {\n totalProfilesV2: _stateLlmV2.profiles.size,\n totalRequestsV2: _stateLlmV2.requests.size,\n maxActiveProfilesPerOwner: _stateLlmV2.maxActiveProfilesPerOwner,\n maxPendingRequestsPerProfile: _stateLlmV2.maxPendingRequestsPerProfile,\n profileIdleMs: _stateLlmV2.profileIdleMs,\n requestStuckMs: _stateLlmV2.requestStuckMs,\n profilesByStatus,\n requestsByStatus,\n };\n}\n\nexport function _resetStateLlmProvidersV2() {\n _stateLlmV2.profiles.clear();\n _stateLlmV2.requests.clear();\n _stateLlmV2.maxActiveProfilesPerOwner = PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER;\n _stateLlmV2.maxPendingRequestsPerProfile =\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE;\n _stateLlmV2.profileIdleMs = PROVIDER_DEFAULT_PROFILE_IDLE_MS;\n _stateLlmV2.requestStuckMs = PROVIDER_DEFAULT_REQUEST_STUCK_MS;\n}\n\n// =====================================================================\n// llm-providers V2 governance overlay (iter22)\n// =====================================================================\nexport const LLMGOV_PROFILE_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n DEGRADED: \"degraded\",\n ARCHIVED: \"archived\",\n});\nexport const LLMGOV_COMPLETION_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n INFERRING: \"inferring\",\n INFERRED: \"inferred\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\nconst _llmgovPTrans = new Map([\n [\n LLMGOV_PROFILE_MATURITY_V2.PENDING,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [LLMGOV_PROFILE_MATURITY_V2.ARCHIVED, new Set()],\n]);\nconst _llmgovPTerminal = new Set([LLMGOV_PROFILE_MATURITY_V2.ARCHIVED]);\nconst _llmgovJTrans = new Map([\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _llmgovPsV2 = new Map();\nconst _llmgovJsV2 = new Map();\nlet _llmgovMaxActive = 8,\n _llmgovMaxPending = 25,\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000,\n _llmgovStuckMs = 60 * 1000;\nfunction _llmgovPos(n, label) {\n const v = Math.floor(Number(n));\n if (!Number.isFinite(v) || v <= 0)\n throw new Error(`${label} must be positive integer`);\n return v;\n}\nfunction _llmgovCheckP(from, to) {\n const a = _llmgovPTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov profile transition ${from} → ${to}`);\n}\nfunction _llmgovCheckJ(from, to) {\n const a = _llmgovJTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov completion transition ${from} → ${to}`);\n}\nfunction _llmgovCountActive(owner) {\n let c = 0;\n for (const p of _llmgovPsV2.values())\n if (p.owner === owner && p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE)\n c++;\n return c;\n}\nfunction _llmgovCountPending(profileId) {\n let c = 0;\n for (const j of _llmgovJsV2.values())\n if (\n j.profileId === profileId &&\n (j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED ||\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING)\n )\n c++;\n return c;\n}\nexport function setMaxActiveLlmgovProfilesPerOwnerV2(n) {\n _llmgovMaxActive = _llmgovPos(n, \"maxActiveLlmgovProfilesPerOwner\");\n}\nexport function getMaxActiveLlmgovProfilesPerOwnerV2() {\n return _llmgovMaxActive;\n}\nexport function setMaxPendingLlmgovCompletionsPerProfileV2(n) {\n _llmgovMaxPending = _llmgovPos(n, \"maxPendingLlmgovCompletionsPerProfile\");\n}\nexport function getMaxPendingLlmgovCompletionsPerProfileV2() {\n return _llmgovMaxPending;\n}\nexport function setLlmgovProfileIdleMsV2(n) {\n _llmgovIdleMs = _llmgovPos(n, \"llmgovProfileIdleMs\");\n}\nexport function getLlmgovProfileIdleMsV2() {\n return _llmgovIdleMs;\n}\nexport function setLlmgovCompletionStuckMsV2(n) {\n _llmgovStuckMs = _llmgovPos(n, \"llmgovCompletionStuckMs\");\n}\nexport function getLlmgovCompletionStuckMsV2() {\n return _llmgovStuckMs;\n}\nexport function _resetStateLlmProvidersGovV2() {\n _llmgovPsV2.clear();\n _llmgovJsV2.clear();\n _llmgovMaxActive = 8;\n _llmgovMaxPending = 25;\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000;\n _llmgovStuckMs = 60 * 1000;\n}\nexport function registerLlmgovProfileV2({\n id,\n owner,\n provider,\n metadata,\n} = {}) {\n if (!id || !owner) throw new Error(\"id and owner required\");\n if (_llmgovPsV2.has(id))\n throw new Error(`llmgov profile ${id} already exists`);\n const now = Date.now();\n const p = {\n id,\n owner,\n provider: provider || \"ollama\",\n status: LLMGOV_PROFILE_MATURITY_V2.PENDING,\n createdAt: now,\n updatedAt: now,\n lastTouchedAt: now,\n activatedAt: null,\n archivedAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovPsV2.set(id, p);\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function activateLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n const isInitial = p.status === LLMGOV_PROFILE_MATURITY_V2.PENDING;\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ACTIVE);\n if (isInitial && _llmgovCountActive(p.owner) >= _llmgovMaxActive)\n throw new Error(`max active llmgov profiles for owner ${p.owner} reached`);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ACTIVE;\n p.updatedAt = now;\n p.lastTouchedAt = now;\n if (!p.activatedAt) p.activatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function degradeLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.DEGRADED);\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = Date.now();\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function archiveLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ARCHIVED);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ARCHIVED;\n p.updatedAt = now;\n if (!p.archivedAt) p.archivedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function touchLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n if (_llmgovPTerminal.has(p.status))\n throw new Error(`cannot touch terminal llmgov profile ${id}`);\n const now = Date.now();\n p.lastTouchedAt = now;\n p.updatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function getLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) return null;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function listLlmgovProfilesV2() {\n return [..._llmgovPsV2.values()].map((p) => ({\n ...p,\n metadata: { ...p.metadata },\n }));\n}\nexport function createLlmgovCompletionV2({\n id,\n profileId,\n model,\n metadata,\n} = {}) {\n if (!id || !profileId) throw new Error(\"id and profileId required\");\n if (_llmgovJsV2.has(id))\n throw new Error(`llmgov completion ${id} already exists`);\n if (!_llmgovPsV2.has(profileId))\n throw new Error(`llmgov profile ${profileId} not found`);\n if (_llmgovCountPending(profileId) >= _llmgovMaxPending)\n throw new Error(\n `max pending llmgov completions for profile ${profileId} reached`,\n );\n const now = Date.now();\n const j = {\n id,\n profileId,\n model: model || \"\",\n status: LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n updatedAt: now,\n startedAt: null,\n settledAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovJsV2.set(id, j);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function inferringLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING;\n j.updatedAt = now;\n if (!j.startedAt) j.startedAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function completeCompletionLlmgovV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function failLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.failReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function cancelLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.cancelReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function getLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) return null;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function listLlmgovCompletionsV2() {\n return [..._llmgovJsV2.values()].map((j) => ({\n ...j,\n metadata: { ...j.metadata },\n }));\n}\nexport function autoDegradeIdleLlmgovProfilesV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const p of _llmgovPsV2.values())\n if (\n p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE &&\n t - p.lastTouchedAt >= _llmgovIdleMs\n ) {\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = t;\n flipped.push(p.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function autoFailStuckLlmgovCompletionsV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const j of _llmgovJsV2.values())\n if (\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING &&\n j.startedAt != null &&\n t - j.startedAt >= _llmgovStuckMs\n ) {\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = t;\n if (!j.settledAt) j.settledAt = t;\n j.metadata.failReason = \"auto-fail-stuck\";\n flipped.push(j.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function getLlmProvidersGovStatsV2() {\n const profilesByStatus = {};\n for (const v of Object.values(LLMGOV_PROFILE_MATURITY_V2))\n profilesByStatus[v] = 0;\n for (const p of _llmgovPsV2.values()) profilesByStatus[p.status]++;\n const completionsByStatus = {};\n for (const v of Object.values(LLMGOV_COMPLETION_LIFECYCLE_V2))\n completionsByStatus[v] = 0;\n for (const j of _llmgovJsV2.values()) completionsByStatus[j.status]++;\n return {\n totalLlmgovProfilesV2: _llmgovPsV2.size,\n totalLlmgovCompletionsV2: _llmgovJsV2.size,\n maxActiveLlmgovProfilesPerOwner: _llmgovMaxActive,\n maxPendingLlmgovCompletionsPerProfile: _llmgovMaxPending,\n llmgovProfileIdleMs: _llmgovIdleMs,\n llmgovCompletionStuckMs: _llmgovStuckMs,\n profilesByStatus,\n completionsByStatus,\n };\n}\n"}, "8a8977603c65fa3d2101c80a11957ed1a111cff8": {"t": 1785754071.2598763, "n": 34755, "h": "05a97e3a6ab73314", "c": "/**\n * LLM Provider registry — supports multiple AI providers with a unified interface.\n */\n\n/**\n * Built-in provider definitions.\n */\nexport const BUILT_IN_PROVIDERS = {\n ollama: {\n name: \"ollama\",\n displayName: \"Ollama (Local)\",\n baseUrl: \"http://localhost:11434\",\n apiKeyEnv: null,\n models: [\n \"qwen2.5:7b\",\n \"qwen2.5:14b\",\n \"qwen2.5-coder:14b\",\n \"qwen2:7b\",\n \"llama3:8b\",\n \"mistral:7b\",\n \"codellama:7b\",\n ],\n free: true,\n },\n openai: {\n name: \"openai\",\n displayName: \"OpenAI\",\n baseUrl: \"https://api.openai.com/v1\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\", \"o1\"],\n free: false,\n },\n anthropic: {\n name: \"anthropic\",\n displayName: \"Anthropic\",\n baseUrl: \"https://api.anthropic.com/v1\",\n apiKeyEnv: \"ANTHROPIC_API_KEY\",\n models: [\n \"claude-opus-4-6\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5-20251001\",\n ],\n free: false,\n },\n deepseek: {\n name: \"deepseek\",\n displayName: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com/v1\",\n apiKeyEnv: \"DEEPSEEK_API_KEY\",\n models: [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"],\n free: false,\n },\n dashscope: {\n name: \"dashscope\",\n displayName: \"DashScope (Alibaba)\",\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n apiKeyEnv: \"DASHSCOPE_API_KEY\",\n models: [\"qwen-turbo\", \"qwen-plus\", \"qwen-max\"],\n free: false,\n },\n gemini: {\n name: \"gemini\",\n displayName: \"Google Gemini\",\n baseUrl: \"https://generativelanguage.googleapis.com/v1beta\",\n apiKeyEnv: \"GEMINI_API_KEY\",\n models: [\"gemini-2.0-flash\", \"gemini-2.0-pro\", \"gemini-1.5-flash\"],\n free: false,\n },\n mistral: {\n name: \"mistral\",\n displayName: \"Mistral AI\",\n baseUrl: \"https://api.mistral.ai/v1\",\n apiKeyEnv: \"MISTRAL_API_KEY\",\n models: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n ],\n free: false,\n },\n volcengine: {\n name: \"volcengine\",\n displayName: \"Volcengine (火山引擎/豆包)\",\n baseUrl: \"https://ark.cn-beijing.volces.com/api/v3\",\n apiKeyEnv: \"VOLCENGINE_API_KEY\",\n models: [\n \"doubao-seed-2-1-pro-260628\",\n \"doubao-seed-2-0-lite-260215\",\n \"doubao-seed-1-6-251015\",\n \"doubao-seed-1-6-flash-250828\",\n \"doubao-seed-1-6-lite-251015\",\n \"doubao-seed-code\",\n ],\n free: false,\n },\n kimi: {\n name: \"kimi\",\n displayName: \"Kimi (月之暗面)\",\n baseUrl: \"https://api.moonshot.cn/v1\",\n apiKeyEnv: \"MOONSHOT_API_KEY\",\n models: [\n \"moonshot-v1-auto\",\n \"moonshot-v1-8k\",\n \"moonshot-v1-32k\",\n \"moonshot-v1-128k\",\n ],\n free: false,\n },\n minimax: {\n name: \"minimax\",\n displayName: \"MiniMax (海螺AI)\",\n baseUrl: \"https://api.minimax.chat/v1\",\n apiKeyEnv: \"MINIMAX_API_KEY\",\n models: [\"MiniMax-Text-01\", \"abab6.5s-chat\", \"abab5.5-chat\"],\n free: false,\n },\n};\n\n// A connectivity probe (`testProvider`) sends a tiny \"Hi\" and expects a quick\n// reply, so it must fail fast — a probe that hangs forever defeats its purpose\n// (it backs `cc llm test`, provider selection, doctor). Bound it with a total\n// timeout; env-overridable via CC_PROVIDER_TEST_TIMEOUT_MS.\nexport const PROVIDER_TEST_TIMEOUT_MS =\n Number(process.env.CC_PROVIDER_TEST_TIMEOUT_MS) || 20000;\n\nasync function _fetchWithTimeout(\n url,\n opts = {},\n ms = PROVIDER_TEST_TIMEOUT_MS,\n) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n if (timer && typeof timer.unref === \"function\") timer.unref();\n try {\n return await fetch(url, { ...opts, signal: controller.signal });\n } catch (e) {\n if (controller.signal.aborted) {\n throw new Error(`provider test timed out after ${ms / 1000}s`);\n }\n throw e;\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Provider registry — manages available providers and active selection.\n */\nexport class LLMProviderRegistry {\n constructor(db) {\n this.db = db;\n this.providers = new Map();\n this._ensureTable();\n this._loadBuiltins();\n this._loadCustom();\n }\n\n _ensureTable() {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS llm_providers (\n name TEXT PRIMARY KEY,\n display_name TEXT NOT NULL,\n base_url TEXT NOT NULL,\n api_key_env TEXT,\n models TEXT DEFAULT '[]',\n is_active INTEGER DEFAULT 0,\n custom INTEGER DEFAULT 0,\n created_at TEXT DEFAULT (datetime('now'))\n )\n `);\n }\n\n _loadBuiltins() {\n for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {\n this.providers.set(name, { ...def, custom: false });\n }\n }\n\n _loadCustom() {\n const rows = this.db\n .prepare(\"SELECT * FROM llm_providers WHERE custom = 1\")\n .all();\n for (const row of rows) {\n // Tolerate a corrupt `models` TEXT column: `|| \"[]\"` only covers null,\n // not malformed JSON, and an unguarded throw here would abort the whole\n // custom-provider load (and the registry constructor) over one bad row.\n let models = [];\n try {\n const parsed = JSON.parse(row.models || \"[]\");\n if (Array.isArray(parsed)) models = parsed;\n } catch {\n /* corrupt models column — fall back to no models for this provider */\n }\n this.providers.set(row.name, {\n name: row.name,\n displayName: row.display_name,\n baseUrl: row.base_url,\n apiKeyEnv: row.api_key_env,\n models,\n custom: true,\n free: false,\n });\n }\n }\n\n /**\n * List all providers.\n */\n list() {\n const result = [];\n for (const [name, provider] of this.providers) {\n const hasKey = provider.apiKeyEnv\n ? !!process.env[provider.apiKeyEnv]\n : true;\n result.push({\n name,\n displayName: provider.displayName,\n baseUrl: provider.baseUrl,\n models: provider.models,\n hasApiKey: hasKey,\n custom: provider.custom || false,\n free: provider.free || false,\n });\n }\n return result;\n }\n\n /**\n * Get a specific provider.\n */\n get(name) {\n return this.providers.get(name) || null;\n }\n\n /**\n * Add a custom provider.\n */\n addProvider(name, config) {\n const provider = {\n name,\n displayName: config.displayName || name,\n baseUrl: config.baseUrl,\n apiKeyEnv: config.apiKeyEnv || null,\n models: config.models || [],\n custom: true,\n free: config.free || false,\n };\n\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, custom) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n provider.displayName,\n provider.baseUrl,\n provider.apiKeyEnv,\n JSON.stringify(provider.models),\n );\n\n this.providers.set(name, provider);\n return provider;\n }\n\n /**\n * Remove a custom provider.\n */\n removeProvider(name) {\n const provider = this.providers.get(name);\n if (!provider) return false;\n if (!provider.custom)\n throw new Error(`Cannot remove built-in provider \"${name}\"`);\n\n this.db\n .prepare(\"DELETE FROM llm_providers WHERE name = ? AND custom = 1\")\n .run(name);\n this.providers.delete(name);\n return true;\n }\n\n /**\n * Get/set the active provider.\n */\n getActive() {\n const row = this.db\n .prepare(\"SELECT name FROM llm_providers WHERE is_active = 1\")\n .get();\n return row ? row.name : \"ollama\";\n }\n\n setActive(name) {\n if (!this.providers.has(name)) {\n throw new Error(`Provider \"${name}\" not found`);\n }\n // Reset all\n this.db.prepare(\"UPDATE llm_providers SET is_active = 0 WHERE 1=1\").run();\n // Set active\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, is_active) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n this.providers.get(name).displayName,\n this.providers.get(name).baseUrl,\n this.providers.get(name).apiKeyEnv,\n JSON.stringify(this.providers.get(name).models),\n );\n return this.providers.get(name);\n }\n\n /**\n * Get API key for a provider (from env or config).\n */\n getApiKey(name) {\n const provider = this.providers.get(name);\n if (!provider) return null;\n if (!provider.apiKeyEnv) return null;\n return process.env[provider.apiKeyEnv] || null;\n }\n\n /**\n * Test provider connectivity.\n */\n async testProvider(name, model) {\n const provider = this.providers.get(name);\n if (!provider) throw new Error(`Provider \"${name}\" not found`);\n\n const start = Date.now();\n const testModel = model || provider.models[0];\n\n if (name === \"ollama\") {\n const res = await _fetchWithTimeout(`${provider.baseUrl}/api/generate`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: testModel, prompt: \"Hi\", stream: false }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n return {\n ok: true,\n elapsed: Date.now() - start,\n response: data.response?.trim(),\n };\n }\n\n if (name === \"gemini\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"GEMINI_API_KEY not set\");\n const res = await _fetchWithTimeout(\n `${provider.baseUrl}/models/${testModel}:generateContent?key=${key}`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ contents: [{ parts: [{ text: \"Hi\" }] }] }),\n },\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n if (name === \"anthropic\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"ANTHROPIC_API_KEY not set\");\n const res = await _fetchWithTimeout(`${provider.baseUrl}/messages`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": key,\n \"anthropic-version\": \"2023-06-01\",\n },\n body: JSON.stringify({\n model: testModel,\n max_tokens: 10,\n messages: [{ role: \"user\", content: \"Hi\" }],\n }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.content?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n // OpenAI-compatible (openai, deepseek, dashscope, mistral, volcengine, kimi, minimax)\n const key = this.getApiKey(name);\n if (!key) throw new Error(`${provider.apiKeyEnv} not set`);\n const res = await _fetchWithTimeout(\n `${provider.baseUrl}/chat/completions`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${key}`,\n },\n body: JSON.stringify({\n model: testModel,\n messages: [{ role: \"user\", content: \"Hi\" }],\n max_tokens: 10,\n }),\n },\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n}\n\n/* ─────────────────────────────────────────────────────────────────\n * V2 Governance Layer (in-memory, independent of SQLite llm_providers)\n *\n * Provider profile maturity: pending → active → suspended → retired\n * - retired terminal\n * - suspended → active recovery (cap-exempt)\n *\n * Request lifecycle: queued → running → completed | failed | cancelled\n * - 3 terminals\n * - per-profile pending-request cap counts queued+running\n *\n * Per-owner active-profile cap on pending→active only (recovery exempt).\n * Per-profile pending-request cap enforced at createRequestV2.\n *\n * Auto-flip:\n * - autoSuspendIdleProfilesV2 active w/ lastSeenAt past idle threshold → suspended\n * - autoFailStuckRequestsV2 running w/ startedAt past stuck threshold → failed\n * ───────────────────────────────────────────────────────────────── */\n\nexport const PROVIDER_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n SUSPENDED: \"suspended\",\n RETIRED: \"retired\",\n});\n\nexport const REQUEST_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n RUNNING: \"running\",\n COMPLETED: \"completed\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\n\nconst _PROVIDER_TRANSITIONS_V2 = new Map([\n [\n PROVIDER_MATURITY_V2.PENDING,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.ACTIVE,\n new Set([PROVIDER_MATURITY_V2.SUSPENDED, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.SUSPENDED,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [PROVIDER_MATURITY_V2.RETIRED, new Set()],\n]);\nconst _PROVIDER_TERMINALS_V2 = new Set([PROVIDER_MATURITY_V2.RETIRED]);\n\nconst _REQUEST_TRANSITIONS_V2 = new Map([\n [\n REQUEST_LIFECYCLE_V2.QUEUED,\n new Set([REQUEST_LIFECYCLE_V2.RUNNING, REQUEST_LIFECYCLE_V2.CANCELLED]),\n ],\n [\n REQUEST_LIFECYCLE_V2.RUNNING,\n new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [REQUEST_LIFECYCLE_V2.COMPLETED, new Set()],\n [REQUEST_LIFECYCLE_V2.FAILED, new Set()],\n [REQUEST_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _REQUEST_TERMINALS_V2 = new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n]);\n\nexport const PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER = 8;\nexport const PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE = 16;\nexport const PROVIDER_DEFAULT_PROFILE_IDLE_MS = 14 * 24 * 60 * 60 * 1000;\nexport const PROVIDER_DEFAULT_REQUEST_STUCK_MS = 5 * 60 * 1000;\n\nconst _stateLlmV2 = {\n profiles: new Map(),\n requests: new Map(),\n maxActiveProfilesPerOwner: PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER,\n maxPendingRequestsPerProfile:\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE,\n profileIdleMs: PROVIDER_DEFAULT_PROFILE_IDLE_MS,\n requestStuckMs: PROVIDER_DEFAULT_REQUEST_STUCK_MS,\n};\n\nfunction _posIntLlmV2(n, label) {\n const v = Math.floor(n);\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`${label} must be a positive integer`);\n }\n return v;\n}\n\nfunction _copyProfileV2(p) {\n return { ...p, metadata: { ...p.metadata } };\n}\n\nfunction _copyRequestV2(r) {\n return { ...r, metadata: { ...r.metadata } };\n}\n\nexport function getMaxActiveProfilesPerOwnerV2() {\n return _stateLlmV2.maxActiveProfilesPerOwner;\n}\n\nexport function setMaxActiveProfilesPerOwnerV2(n) {\n _stateLlmV2.maxActiveProfilesPerOwner = _posIntLlmV2(\n n,\n \"maxActiveProfilesPerOwner\",\n );\n}\n\nexport function getMaxPendingRequestsPerProfileV2() {\n return _stateLlmV2.maxPendingRequestsPerProfile;\n}\n\nexport function setMaxPendingRequestsPerProfileV2(n) {\n _stateLlmV2.maxPendingRequestsPerProfile = _posIntLlmV2(\n n,\n \"maxPendingRequestsPerProfile\",\n );\n}\n\nexport function getProfileIdleMsV2() {\n return _stateLlmV2.profileIdleMs;\n}\n\nexport function setProfileIdleMsV2(ms) {\n _stateLlmV2.profileIdleMs = _posIntLlmV2(ms, \"profileIdleMs\");\n}\n\nexport function getRequestStuckMsV2() {\n return _stateLlmV2.requestStuckMs;\n}\n\nexport function setRequestStuckMsV2(ms) {\n _stateLlmV2.requestStuckMs = _posIntLlmV2(ms, \"requestStuckMs\");\n}\n\nexport function getActiveProfileCountV2(ownerId) {\n let count = 0;\n for (const p of _stateLlmV2.profiles.values()) {\n if (p.ownerId === ownerId && p.status === PROVIDER_MATURITY_V2.ACTIVE) {\n count++;\n }\n }\n return count;\n}\n\nexport function getPendingRequestCountV2(profileId) {\n let count = 0;\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.profileId === profileId &&\n (r.status === REQUEST_LIFECYCLE_V2.QUEUED ||\n r.status === REQUEST_LIFECYCLE_V2.RUNNING)\n ) {\n count++;\n }\n }\n return count;\n}\n\nexport function registerProfileV2(\n id,\n { ownerId, provider, model, metadata } = {},\n) {\n if (!id) throw new Error(\"profile id is required\");\n if (!ownerId) throw new Error(\"ownerId is required\");\n if (!provider) throw new Error(\"provider is required\");\n if (_stateLlmV2.profiles.has(id))\n throw new Error(`profile ${id} already exists`);\n const now = Date.now();\n const profile = {\n id,\n ownerId,\n provider,\n model: model || \"default\",\n status: PROVIDER_MATURITY_V2.PENDING,\n createdAt: now,\n lastSeenAt: now,\n activatedAt: null,\n retiredAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.profiles.set(id, profile);\n return _copyProfileV2(profile);\n}\n\nexport function getProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n return p ? _copyProfileV2(p) : null;\n}\n\nexport function listProfilesV2({ ownerId, status, provider } = {}) {\n const out = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (ownerId && p.ownerId !== ownerId) continue;\n if (status && p.status !== status) continue;\n if (provider && p.provider !== provider) continue;\n out.push(_copyProfileV2(p));\n }\n return out;\n}\n\nexport function setProfileStatusV2(id, next) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n const allowed = _PROVIDER_TRANSITIONS_V2.get(p.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid profile transition: ${p.status} → ${next}`);\n }\n if (\n p.status === PROVIDER_MATURITY_V2.PENDING &&\n next === PROVIDER_MATURITY_V2.ACTIVE\n ) {\n const count = getActiveProfileCountV2(p.ownerId);\n if (count >= _stateLlmV2.maxActiveProfilesPerOwner) {\n throw new Error(\n `owner ${p.ownerId} active-profile cap reached (${count}/${_stateLlmV2.maxActiveProfilesPerOwner})`,\n );\n }\n }\n const now = Date.now();\n p.status = next;\n p.lastSeenAt = now;\n if (next === PROVIDER_MATURITY_V2.ACTIVE && !p.activatedAt)\n p.activatedAt = now;\n if (_PROVIDER_TERMINALS_V2.has(next) && !p.retiredAt) p.retiredAt = now;\n return _copyProfileV2(p);\n}\n\nexport function activateProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.ACTIVE);\n}\n\nexport function suspendProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.SUSPENDED);\n}\n\nexport function retireProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.RETIRED);\n}\n\nexport function touchProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n p.lastSeenAt = Date.now();\n return _copyProfileV2(p);\n}\n\nexport function createRequestV2(id, { profileId, kind, metadata } = {}) {\n if (!id) throw new Error(\"request id is required\");\n if (!profileId) throw new Error(\"profileId is required\");\n if (_stateLlmV2.requests.has(id))\n throw new Error(`request ${id} already exists`);\n const profile = _stateLlmV2.profiles.get(profileId);\n if (!profile) throw new Error(`profile ${profileId} not found`);\n const pending = getPendingRequestCountV2(profileId);\n if (pending >= _stateLlmV2.maxPendingRequestsPerProfile) {\n throw new Error(\n `profile ${profileId} pending-request cap reached (${pending}/${_stateLlmV2.maxPendingRequestsPerProfile})`,\n );\n }\n const now = Date.now();\n const req = {\n id,\n profileId,\n kind: kind || \"completion\",\n status: REQUEST_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n lastSeenAt: now,\n startedAt: null,\n settledAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.requests.set(id, req);\n return _copyRequestV2(req);\n}\n\nexport function getRequestV2(id) {\n const r = _stateLlmV2.requests.get(id);\n return r ? _copyRequestV2(r) : null;\n}\n\nexport function listRequestsV2({ profileId, status } = {}) {\n const out = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (profileId && r.profileId !== profileId) continue;\n if (status && r.status !== status) continue;\n out.push(_copyRequestV2(r));\n }\n return out;\n}\n\nexport function setRequestStatusV2(id, next) {\n const r = _stateLlmV2.requests.get(id);\n if (!r) throw new Error(`request ${id} not found`);\n const allowed = _REQUEST_TRANSITIONS_V2.get(r.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid request transition: ${r.status} → ${next}`);\n }\n const now = Date.now();\n r.status = next;\n r.lastSeenAt = now;\n if (next === REQUEST_LIFECYCLE_V2.RUNNING && !r.startedAt) r.startedAt = now;\n if (_REQUEST_TERMINALS_V2.has(next) && !r.settledAt) r.settledAt = now;\n return _copyRequestV2(r);\n}\n\nexport function startRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.RUNNING);\n}\n\nexport function completeRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.COMPLETED);\n}\n\nexport function failRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.FAILED);\n}\n\nexport function cancelRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.CANCELLED);\n}\n\nexport function autoSuspendIdleProfilesV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (\n p.status === PROVIDER_MATURITY_V2.ACTIVE &&\n now - p.lastSeenAt >= _stateLlmV2.profileIdleMs\n ) {\n p.status = PROVIDER_MATURITY_V2.SUSPENDED;\n p.lastSeenAt = now;\n flipped.push(p.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function autoFailStuckRequestsV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.status === REQUEST_LIFECYCLE_V2.RUNNING &&\n r.startedAt &&\n now - r.startedAt >= _stateLlmV2.requestStuckMs\n ) {\n r.status = REQUEST_LIFECYCLE_V2.FAILED;\n r.lastSeenAt = now;\n if (!r.settledAt) r.settledAt = now;\n flipped.push(r.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function getLlmProvidersStatsV2() {\n const profilesByStatus = {};\n for (const s of Object.values(PROVIDER_MATURITY_V2)) profilesByStatus[s] = 0;\n for (const p of _stateLlmV2.profiles.values()) profilesByStatus[p.status]++;\n const requestsByStatus = {};\n for (const s of Object.values(REQUEST_LIFECYCLE_V2)) requestsByStatus[s] = 0;\n for (const r of _stateLlmV2.requests.values()) requestsByStatus[r.status]++;\n return {\n totalProfilesV2: _stateLlmV2.profiles.size,\n totalRequestsV2: _stateLlmV2.requests.size,\n maxActiveProfilesPerOwner: _stateLlmV2.maxActiveProfilesPerOwner,\n maxPendingRequestsPerProfile: _stateLlmV2.maxPendingRequestsPerProfile,\n profileIdleMs: _stateLlmV2.profileIdleMs,\n requestStuckMs: _stateLlmV2.requestStuckMs,\n profilesByStatus,\n requestsByStatus,\n };\n}\n\nexport function _resetStateLlmProvidersV2() {\n _stateLlmV2.profiles.clear();\n _stateLlmV2.requests.clear();\n _stateLlmV2.maxActiveProfilesPerOwner = PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER;\n _stateLlmV2.maxPendingRequestsPerProfile =\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE;\n _stateLlmV2.profileIdleMs = PROVIDER_DEFAULT_PROFILE_IDLE_MS;\n _stateLlmV2.requestStuckMs = PROVIDER_DEFAULT_REQUEST_STUCK_MS;\n}\n\n// =====================================================================\n// llm-providers V2 governance overlay (iter22)\n// =====================================================================\nexport const LLMGOV_PROFILE_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n DEGRADED: \"degraded\",\n ARCHIVED: \"archived\",\n});\nexport const LLMGOV_COMPLETION_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n INFERRING: \"inferring\",\n INFERRED: \"inferred\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\nconst _llmgovPTrans = new Map([\n [\n LLMGOV_PROFILE_MATURITY_V2.PENDING,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [LLMGOV_PROFILE_MATURITY_V2.ARCHIVED, new Set()],\n]);\nconst _llmgovPTerminal = new Set([LLMGOV_PROFILE_MATURITY_V2.ARCHIVED]);\nconst _llmgovJTrans = new Map([\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _llmgovPsV2 = new Map();\nconst _llmgovJsV2 = new Map();\nlet _llmgovMaxActive = 8,\n _llmgovMaxPending = 25,\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000,\n _llmgovStuckMs = 60 * 1000;\nfunction _llmgovPos(n, label) {\n const v = Math.floor(Number(n));\n if (!Number.isFinite(v) || v <= 0)\n throw new Error(`${label} must be positive integer`);\n return v;\n}\nfunction _llmgovCheckP(from, to) {\n const a = _llmgovPTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov profile transition ${from} → ${to}`);\n}\nfunction _llmgovCheckJ(from, to) {\n const a = _llmgovJTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov completion transition ${from} → ${to}`);\n}\nfunction _llmgovCountActive(owner) {\n let c = 0;\n for (const p of _llmgovPsV2.values())\n if (p.owner === owner && p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE)\n c++;\n return c;\n}\nfunction _llmgovCountPending(profileId) {\n let c = 0;\n for (const j of _llmgovJsV2.values())\n if (\n j.profileId === profileId &&\n (j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED ||\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING)\n )\n c++;\n return c;\n}\nexport function setMaxActiveLlmgovProfilesPerOwnerV2(n) {\n _llmgovMaxActive = _llmgovPos(n, \"maxActiveLlmgovProfilesPerOwner\");\n}\nexport function getMaxActiveLlmgovProfilesPerOwnerV2() {\n return _llmgovMaxActive;\n}\nexport function setMaxPendingLlmgovCompletionsPerProfileV2(n) {\n _llmgovMaxPending = _llmgovPos(n, \"maxPendingLlmgovCompletionsPerProfile\");\n}\nexport function getMaxPendingLlmgovCompletionsPerProfileV2() {\n return _llmgovMaxPending;\n}\nexport function setLlmgovProfileIdleMsV2(n) {\n _llmgovIdleMs = _llmgovPos(n, \"llmgovProfileIdleMs\");\n}\nexport function getLlmgovProfileIdleMsV2() {\n return _llmgovIdleMs;\n}\nexport function setLlmgovCompletionStuckMsV2(n) {\n _llmgovStuckMs = _llmgovPos(n, \"llmgovCompletionStuckMs\");\n}\nexport function getLlmgovCompletionStuckMsV2() {\n return _llmgovStuckMs;\n}\nexport function _resetStateLlmProvidersGovV2() {\n _llmgovPsV2.clear();\n _llmgovJsV2.clear();\n _llmgovMaxActive = 8;\n _llmgovMaxPending = 25;\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000;\n _llmgovStuckMs = 60 * 1000;\n}\nexport function registerLlmgovProfileV2({\n id,\n owner,\n provider,\n metadata,\n} = {}) {\n if (!id || !owner) throw new Error(\"id and owner required\");\n if (_llmgovPsV2.has(id))\n throw new Error(`llmgov profile ${id} already exists`);\n const now = Date.now();\n const p = {\n id,\n owner,\n provider: provider || \"ollama\",\n status: LLMGOV_PROFILE_MATURITY_V2.PENDING,\n createdAt: now,\n updatedAt: now,\n lastTouchedAt: now,\n activatedAt: null,\n archivedAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovPsV2.set(id, p);\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function activateLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n const isInitial = p.status === LLMGOV_PROFILE_MATURITY_V2.PENDING;\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ACTIVE);\n if (isInitial && _llmgovCountActive(p.owner) >= _llmgovMaxActive)\n throw new Error(`max active llmgov profiles for owner ${p.owner} reached`);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ACTIVE;\n p.updatedAt = now;\n p.lastTouchedAt = now;\n if (!p.activatedAt) p.activatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function degradeLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.DEGRADED);\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = Date.now();\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function archiveLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ARCHIVED);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ARCHIVED;\n p.updatedAt = now;\n if (!p.archivedAt) p.archivedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function touchLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n if (_llmgovPTerminal.has(p.status))\n throw new Error(`cannot touch terminal llmgov profile ${id}`);\n const now = Date.now();\n p.lastTouchedAt = now;\n p.updatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function getLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) return null;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function listLlmgovProfilesV2() {\n return [..._llmgovPsV2.values()].map((p) => ({\n ...p,\n metadata: { ...p.metadata },\n }));\n}\nexport function createLlmgovCompletionV2({\n id,\n profileId,\n model,\n metadata,\n} = {}) {\n if (!id || !profileId) throw new Error(\"id and profileId required\");\n if (_llmgovJsV2.has(id))\n throw new Error(`llmgov completion ${id} already exists`);\n if (!_llmgovPsV2.has(profileId))\n throw new Error(`llmgov profile ${profileId} not found`);\n if (_llmgovCountPending(profileId) >= _llmgovMaxPending)\n throw new Error(\n `max pending llmgov completions for profile ${profileId} reached`,\n );\n const now = Date.now();\n const j = {\n id,\n profileId,\n model: model || \"\",\n status: LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n updatedAt: now,\n startedAt: null,\n settledAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovJsV2.set(id, j);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function inferringLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING;\n j.updatedAt = now;\n if (!j.startedAt) j.startedAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function completeCompletionLlmgovV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function failLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.failReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function cancelLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.cancelReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function getLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) return null;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function listLlmgovCompletionsV2() {\n return [..._llmgovJsV2.values()].map((j) => ({\n ...j,\n metadata: { ...j.metadata },\n }));\n}\nexport function autoDegradeIdleLlmgovProfilesV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const p of _llmgovPsV2.values())\n if (\n p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE &&\n t - p.lastTouchedAt >= _llmgovIdleMs\n ) {\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = t;\n flipped.push(p.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function autoFailStuckLlmgovCompletionsV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const j of _llmgovJsV2.values())\n if (\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING &&\n j.startedAt != null &&\n t - j.startedAt >= _llmgovStuckMs\n ) {\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = t;\n if (!j.settledAt) j.settledAt = t;\n j.metadata.failReason = \"auto-fail-stuck\";\n flipped.push(j.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function getLlmProvidersGovStatsV2() {\n const profilesByStatus = {};\n for (const v of Object.values(LLMGOV_PROFILE_MATURITY_V2))\n profilesByStatus[v] = 0;\n for (const p of _llmgovPsV2.values()) profilesByStatus[p.status]++;\n const completionsByStatus = {};\n for (const v of Object.values(LLMGOV_COMPLETION_LIFECYCLE_V2))\n completionsByStatus[v] = 0;\n for (const j of _llmgovJsV2.values()) completionsByStatus[j.status]++;\n return {\n totalLlmgovProfilesV2: _llmgovPsV2.size,\n totalLlmgovCompletionsV2: _llmgovJsV2.size,\n maxActiveLlmgovProfilesPerOwner: _llmgovMaxActive,\n maxPendingLlmgovCompletionsPerProfile: _llmgovMaxPending,\n llmgovProfileIdleMs: _llmgovIdleMs,\n llmgovCompletionStuckMs: _llmgovStuckMs,\n profilesByStatus,\n completionsByStatus,\n };\n}\n"}, "a7b3a322836dbd5280ef7738c327bc3668e25c45": {"t": 1785754072.6995075, "n": 34306, "h": "e2cfb25c69e417d4", "c": "/**\n * LLM Provider registry — supports multiple AI providers with a unified interface.\n */\n\n/**\n * Built-in provider definitions.\n */\nexport const BUILT_IN_PROVIDERS = {\n ollama: {\n name: \"ollama\",\n displayName: \"Ollama (Local)\",\n baseUrl: \"http://localhost:11434\",\n apiKeyEnv: null,\n models: [\n \"qwen2.5:7b\",\n \"qwen2.5:14b\",\n \"qwen2.5-coder:14b\",\n \"qwen2:7b\",\n \"llama3:8b\",\n \"mistral:7b\",\n \"codellama:7b\",\n ],\n free: true,\n },\n openai: {\n name: \"openai\",\n displayName: \"OpenAI\",\n baseUrl: \"https://api.openai.com/v1\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\", \"o1\"],\n free: false,\n },\n anthropic: {\n name: \"anthropic\",\n displayName: \"Anthropic\",\n baseUrl: \"https://api.anthropic.com/v1\",\n apiKeyEnv: \"ANTHROPIC_API_KEY\",\n models: [\n \"claude-opus-4-6\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5-20251001\",\n ],\n free: false,\n },\n deepseek: {\n name: \"deepseek\",\n displayName: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com/v1\",\n apiKeyEnv: \"DEEPSEEK_API_KEY\",\n models: [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"],\n free: false,\n },\n dashscope: {\n name: \"dashscope\",\n displayName: \"DashScope (Alibaba)\",\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n apiKeyEnv: \"DASHSCOPE_API_KEY\",\n models: [\"qwen-turbo\", \"qwen-plus\", \"qwen-max\"],\n free: false,\n },\n gemini: {\n name: \"gemini\",\n displayName: \"Google Gemini\",\n baseUrl: \"https://generativelanguage.googleapis.com/v1beta\",\n apiKeyEnv: \"GEMINI_API_KEY\",\n models: [\"gemini-2.0-flash\", \"gemini-2.0-pro\", \"gemini-1.5-flash\"],\n free: false,\n },\n mistral: {\n name: \"mistral\",\n displayName: \"Mistral AI\",\n baseUrl: \"https://api.mistral.ai/v1\",\n apiKeyEnv: \"MISTRAL_API_KEY\",\n models: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n ],\n free: false,\n },\n volcengine: {\n name: \"volcengine\",\n displayName: \"Volcengine (火山引擎/豆包)\",\n baseUrl: \"https://ark.cn-beijing.volces.com/api/v3\",\n apiKeyEnv: \"VOLCENGINE_API_KEY\",\n models: [\n \"doubao-seed-2-1-pro-260628\",\n \"doubao-seed-2-0-lite-260215\",\n \"doubao-seed-1-6-251015\",\n \"doubao-seed-1-6-flash-250828\",\n \"doubao-seed-1-6-lite-251015\",\n \"doubao-seed-code\",\n ],\n free: false,\n },\n kimi: {\n name: \"kimi\",\n displayName: \"Kimi (月之暗面)\",\n baseUrl: \"https://api.moonshot.cn/v1\",\n apiKeyEnv: \"MOONSHOT_API_KEY\",\n models: [\n \"moonshot-v1-auto\",\n \"moonshot-v1-8k\",\n \"moonshot-v1-32k\",\n \"moonshot-v1-128k\",\n ],\n free: false,\n },\n minimax: {\n name: \"minimax\",\n displayName: \"MiniMax (海螺AI)\",\n baseUrl: \"https://api.minimax.chat/v1\",\n apiKeyEnv: \"MINIMAX_API_KEY\",\n models: [\"MiniMax-Text-01\", \"abab6.5s-chat\", \"abab5.5-chat\"],\n free: false,\n },\n};\n\n// A connectivity probe (`testProvider`) sends a tiny \"Hi\" and expects a quick\n// reply, so it must fail fast — a probe that hangs forever defeats its purpose\n// (it backs `cc llm test`, provider selection, doctor). Bound it with a total\n// timeout; env-overridable via CC_PROVIDER_TEST_TIMEOUT_MS.\nexport const PROVIDER_TEST_TIMEOUT_MS =\n Number(process.env.CC_PROVIDER_TEST_TIMEOUT_MS) || 20000;\n\nasync function _fetchWithTimeout(\n url,\n opts = {},\n ms = PROVIDER_TEST_TIMEOUT_MS,\n) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n if (timer && typeof timer.unref === \"function\") timer.unref();\n try {\n return await fetch(url, { ...opts, signal: controller.signal });\n } catch (e) {\n if (controller.signal.aborted) {\n throw new Error(`provider test timed out after ${ms / 1000}s`);\n }\n throw e;\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Provider registry — manages available providers and active selection.\n */\nexport class LLMProviderRegistry {\n constructor(db) {\n this.db = db;\n this.providers = new Map();\n this._ensureTable();\n this._loadBuiltins();\n this._loadCustom();\n }\n\n _ensureTable() {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS llm_providers (\n name TEXT PRIMARY KEY,\n display_name TEXT NOT NULL,\n base_url TEXT NOT NULL,\n api_key_env TEXT,\n models TEXT DEFAULT '[]',\n is_active INTEGER DEFAULT 0,\n custom INTEGER DEFAULT 0,\n created_at TEXT DEFAULT (datetime('now'))\n )\n `);\n }\n\n _loadBuiltins() {\n for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {\n this.providers.set(name, { ...def, custom: false });\n }\n }\n\n _loadCustom() {\n const rows = this.db\n .prepare(\"SELECT * FROM llm_providers WHERE custom = 1\")\n .all();\n for (const row of rows) {\n this.providers.set(row.name, {\n name: row.name,\n displayName: row.display_name,\n baseUrl: row.base_url,\n apiKeyEnv: row.api_key_env,\n models: JSON.parse(row.models || \"[]\"),\n custom: true,\n free: false,\n });\n }\n }\n\n /**\n * List all providers.\n */\n list() {\n const result = [];\n for (const [name, provider] of this.providers) {\n const hasKey = provider.apiKeyEnv\n ? !!process.env[provider.apiKeyEnv]\n : true;\n result.push({\n name,\n displayName: provider.displayName,\n baseUrl: provider.baseUrl,\n models: provider.models,\n hasApiKey: hasKey,\n custom: provider.custom || false,\n free: provider.free || false,\n });\n }\n return result;\n }\n\n /**\n * Get a specific provider.\n */\n get(name) {\n return this.providers.get(name) || null;\n }\n\n /**\n * Add a custom provider.\n */\n addProvider(name, config) {\n const provider = {\n name,\n displayName: config.displayName || name,\n baseUrl: config.baseUrl,\n apiKeyEnv: config.apiKeyEnv || null,\n models: config.models || [],\n custom: true,\n free: config.free || false,\n };\n\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, custom) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n provider.displayName,\n provider.baseUrl,\n provider.apiKeyEnv,\n JSON.stringify(provider.models),\n );\n\n this.providers.set(name, provider);\n return provider;\n }\n\n /**\n * Remove a custom provider.\n */\n removeProvider(name) {\n const provider = this.providers.get(name);\n if (!provider) return false;\n if (!provider.custom)\n throw new Error(`Cannot remove built-in provider \"${name}\"`);\n\n this.db\n .prepare(\"DELETE FROM llm_providers WHERE name = ? AND custom = 1\")\n .run(name);\n this.providers.delete(name);\n return true;\n }\n\n /**\n * Get/set the active provider.\n */\n getActive() {\n const row = this.db\n .prepare(\"SELECT name FROM llm_providers WHERE is_active = 1\")\n .get();\n return row ? row.name : \"ollama\";\n }\n\n setActive(name) {\n if (!this.providers.has(name)) {\n throw new Error(`Provider \"${name}\" not found`);\n }\n // Reset all\n this.db.prepare(\"UPDATE llm_providers SET is_active = 0 WHERE 1=1\").run();\n // Set active\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, is_active) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n this.providers.get(name).displayName,\n this.providers.get(name).baseUrl,\n this.providers.get(name).apiKeyEnv,\n JSON.stringify(this.providers.get(name).models),\n );\n return this.providers.get(name);\n }\n\n /**\n * Get API key for a provider (from env or config).\n */\n getApiKey(name) {\n const provider = this.providers.get(name);\n if (!provider) return null;\n if (!provider.apiKeyEnv) return null;\n return process.env[provider.apiKeyEnv] || null;\n }\n\n /**\n * Test provider connectivity.\n */\n async testProvider(name, model) {\n const provider = this.providers.get(name);\n if (!provider) throw new Error(`Provider \"${name}\" not found`);\n\n const start = Date.now();\n const testModel = model || provider.models[0];\n\n if (name === \"ollama\") {\n const res = await _fetchWithTimeout(`${provider.baseUrl}/api/generate`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: testModel, prompt: \"Hi\", stream: false }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n return {\n ok: true,\n elapsed: Date.now() - start,\n response: data.response?.trim(),\n };\n }\n\n if (name === \"gemini\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"GEMINI_API_KEY not set\");\n const res = await _fetchWithTimeout(\n `${provider.baseUrl}/models/${testModel}:generateContent?key=${key}`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ contents: [{ parts: [{ text: \"Hi\" }] }] }),\n },\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n if (name === \"anthropic\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"ANTHROPIC_API_KEY not set\");\n const res = await _fetchWithTimeout(`${provider.baseUrl}/messages`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": key,\n \"anthropic-version\": \"2023-06-01\",\n },\n body: JSON.stringify({\n model: testModel,\n max_tokens: 10,\n messages: [{ role: \"user\", content: \"Hi\" }],\n }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.content?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n // OpenAI-compatible (openai, deepseek, dashscope, mistral, volcengine, kimi, minimax)\n const key = this.getApiKey(name);\n if (!key) throw new Error(`${provider.apiKeyEnv} not set`);\n const res = await _fetchWithTimeout(\n `${provider.baseUrl}/chat/completions`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${key}`,\n },\n body: JSON.stringify({\n model: testModel,\n messages: [{ role: \"user\", content: \"Hi\" }],\n max_tokens: 10,\n }),\n },\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n}\n\n/* ─────────────────────────────────────────────────────────────────\n * V2 Governance Layer (in-memory, independent of SQLite llm_providers)\n *\n * Provider profile maturity: pending → active → suspended → retired\n * - retired terminal\n * - suspended → active recovery (cap-exempt)\n *\n * Request lifecycle: queued → running → completed | failed | cancelled\n * - 3 terminals\n * - per-profile pending-request cap counts queued+running\n *\n * Per-owner active-profile cap on pending→active only (recovery exempt).\n * Per-profile pending-request cap enforced at createRequestV2.\n *\n * Auto-flip:\n * - autoSuspendIdleProfilesV2 active w/ lastSeenAt past idle threshold → suspended\n * - autoFailStuckRequestsV2 running w/ startedAt past stuck threshold → failed\n * ───────────────────────────────────────────────────────────────── */\n\nexport const PROVIDER_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n SUSPENDED: \"suspended\",\n RETIRED: \"retired\",\n});\n\nexport const REQUEST_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n RUNNING: \"running\",\n COMPLETED: \"completed\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\n\nconst _PROVIDER_TRANSITIONS_V2 = new Map([\n [\n PROVIDER_MATURITY_V2.PENDING,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.ACTIVE,\n new Set([PROVIDER_MATURITY_V2.SUSPENDED, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.SUSPENDED,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [PROVIDER_MATURITY_V2.RETIRED, new Set()],\n]);\nconst _PROVIDER_TERMINALS_V2 = new Set([PROVIDER_MATURITY_V2.RETIRED]);\n\nconst _REQUEST_TRANSITIONS_V2 = new Map([\n [\n REQUEST_LIFECYCLE_V2.QUEUED,\n new Set([REQUEST_LIFECYCLE_V2.RUNNING, REQUEST_LIFECYCLE_V2.CANCELLED]),\n ],\n [\n REQUEST_LIFECYCLE_V2.RUNNING,\n new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [REQUEST_LIFECYCLE_V2.COMPLETED, new Set()],\n [REQUEST_LIFECYCLE_V2.FAILED, new Set()],\n [REQUEST_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _REQUEST_TERMINALS_V2 = new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n]);\n\nexport const PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER = 8;\nexport const PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE = 16;\nexport const PROVIDER_DEFAULT_PROFILE_IDLE_MS = 14 * 24 * 60 * 60 * 1000;\nexport const PROVIDER_DEFAULT_REQUEST_STUCK_MS = 5 * 60 * 1000;\n\nconst _stateLlmV2 = {\n profiles: new Map(),\n requests: new Map(),\n maxActiveProfilesPerOwner: PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER,\n maxPendingRequestsPerProfile:\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE,\n profileIdleMs: PROVIDER_DEFAULT_PROFILE_IDLE_MS,\n requestStuckMs: PROVIDER_DEFAULT_REQUEST_STUCK_MS,\n};\n\nfunction _posIntLlmV2(n, label) {\n const v = Math.floor(n);\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`${label} must be a positive integer`);\n }\n return v;\n}\n\nfunction _copyProfileV2(p) {\n return { ...p, metadata: { ...p.metadata } };\n}\n\nfunction _copyRequestV2(r) {\n return { ...r, metadata: { ...r.metadata } };\n}\n\nexport function getMaxActiveProfilesPerOwnerV2() {\n return _stateLlmV2.maxActiveProfilesPerOwner;\n}\n\nexport function setMaxActiveProfilesPerOwnerV2(n) {\n _stateLlmV2.maxActiveProfilesPerOwner = _posIntLlmV2(\n n,\n \"maxActiveProfilesPerOwner\",\n );\n}\n\nexport function getMaxPendingRequestsPerProfileV2() {\n return _stateLlmV2.maxPendingRequestsPerProfile;\n}\n\nexport function setMaxPendingRequestsPerProfileV2(n) {\n _stateLlmV2.maxPendingRequestsPerProfile = _posIntLlmV2(\n n,\n \"maxPendingRequestsPerProfile\",\n );\n}\n\nexport function getProfileIdleMsV2() {\n return _stateLlmV2.profileIdleMs;\n}\n\nexport function setProfileIdleMsV2(ms) {\n _stateLlmV2.profileIdleMs = _posIntLlmV2(ms, \"profileIdleMs\");\n}\n\nexport function getRequestStuckMsV2() {\n return _stateLlmV2.requestStuckMs;\n}\n\nexport function setRequestStuckMsV2(ms) {\n _stateLlmV2.requestStuckMs = _posIntLlmV2(ms, \"requestStuckMs\");\n}\n\nexport function getActiveProfileCountV2(ownerId) {\n let count = 0;\n for (const p of _stateLlmV2.profiles.values()) {\n if (p.ownerId === ownerId && p.status === PROVIDER_MATURITY_V2.ACTIVE) {\n count++;\n }\n }\n return count;\n}\n\nexport function getPendingRequestCountV2(profileId) {\n let count = 0;\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.profileId === profileId &&\n (r.status === REQUEST_LIFECYCLE_V2.QUEUED ||\n r.status === REQUEST_LIFECYCLE_V2.RUNNING)\n ) {\n count++;\n }\n }\n return count;\n}\n\nexport function registerProfileV2(\n id,\n { ownerId, provider, model, metadata } = {},\n) {\n if (!id) throw new Error(\"profile id is required\");\n if (!ownerId) throw new Error(\"ownerId is required\");\n if (!provider) throw new Error(\"provider is required\");\n if (_stateLlmV2.profiles.has(id))\n throw new Error(`profile ${id} already exists`);\n const now = Date.now();\n const profile = {\n id,\n ownerId,\n provider,\n model: model || \"default\",\n status: PROVIDER_MATURITY_V2.PENDING,\n createdAt: now,\n lastSeenAt: now,\n activatedAt: null,\n retiredAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.profiles.set(id, profile);\n return _copyProfileV2(profile);\n}\n\nexport function getProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n return p ? _copyProfileV2(p) : null;\n}\n\nexport function listProfilesV2({ ownerId, status, provider } = {}) {\n const out = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (ownerId && p.ownerId !== ownerId) continue;\n if (status && p.status !== status) continue;\n if (provider && p.provider !== provider) continue;\n out.push(_copyProfileV2(p));\n }\n return out;\n}\n\nexport function setProfileStatusV2(id, next) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n const allowed = _PROVIDER_TRANSITIONS_V2.get(p.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid profile transition: ${p.status} → ${next}`);\n }\n if (\n p.status === PROVIDER_MATURITY_V2.PENDING &&\n next === PROVIDER_MATURITY_V2.ACTIVE\n ) {\n const count = getActiveProfileCountV2(p.ownerId);\n if (count >= _stateLlmV2.maxActiveProfilesPerOwner) {\n throw new Error(\n `owner ${p.ownerId} active-profile cap reached (${count}/${_stateLlmV2.maxActiveProfilesPerOwner})`,\n );\n }\n }\n const now = Date.now();\n p.status = next;\n p.lastSeenAt = now;\n if (next === PROVIDER_MATURITY_V2.ACTIVE && !p.activatedAt)\n p.activatedAt = now;\n if (_PROVIDER_TERMINALS_V2.has(next) && !p.retiredAt) p.retiredAt = now;\n return _copyProfileV2(p);\n}\n\nexport function activateProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.ACTIVE);\n}\n\nexport function suspendProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.SUSPENDED);\n}\n\nexport function retireProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.RETIRED);\n}\n\nexport function touchProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n p.lastSeenAt = Date.now();\n return _copyProfileV2(p);\n}\n\nexport function createRequestV2(id, { profileId, kind, metadata } = {}) {\n if (!id) throw new Error(\"request id is required\");\n if (!profileId) throw new Error(\"profileId is required\");\n if (_stateLlmV2.requests.has(id))\n throw new Error(`request ${id} already exists`);\n const profile = _stateLlmV2.profiles.get(profileId);\n if (!profile) throw new Error(`profile ${profileId} not found`);\n const pending = getPendingRequestCountV2(profileId);\n if (pending >= _stateLlmV2.maxPendingRequestsPerProfile) {\n throw new Error(\n `profile ${profileId} pending-request cap reached (${pending}/${_stateLlmV2.maxPendingRequestsPerProfile})`,\n );\n }\n const now = Date.now();\n const req = {\n id,\n profileId,\n kind: kind || \"completion\",\n status: REQUEST_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n lastSeenAt: now,\n startedAt: null,\n settledAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.requests.set(id, req);\n return _copyRequestV2(req);\n}\n\nexport function getRequestV2(id) {\n const r = _stateLlmV2.requests.get(id);\n return r ? _copyRequestV2(r) : null;\n}\n\nexport function listRequestsV2({ profileId, status } = {}) {\n const out = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (profileId && r.profileId !== profileId) continue;\n if (status && r.status !== status) continue;\n out.push(_copyRequestV2(r));\n }\n return out;\n}\n\nexport function setRequestStatusV2(id, next) {\n const r = _stateLlmV2.requests.get(id);\n if (!r) throw new Error(`request ${id} not found`);\n const allowed = _REQUEST_TRANSITIONS_V2.get(r.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid request transition: ${r.status} → ${next}`);\n }\n const now = Date.now();\n r.status = next;\n r.lastSeenAt = now;\n if (next === REQUEST_LIFECYCLE_V2.RUNNING && !r.startedAt) r.startedAt = now;\n if (_REQUEST_TERMINALS_V2.has(next) && !r.settledAt) r.settledAt = now;\n return _copyRequestV2(r);\n}\n\nexport function startRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.RUNNING);\n}\n\nexport function completeRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.COMPLETED);\n}\n\nexport function failRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.FAILED);\n}\n\nexport function cancelRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.CANCELLED);\n}\n\nexport function autoSuspendIdleProfilesV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (\n p.status === PROVIDER_MATURITY_V2.ACTIVE &&\n now - p.lastSeenAt >= _stateLlmV2.profileIdleMs\n ) {\n p.status = PROVIDER_MATURITY_V2.SUSPENDED;\n p.lastSeenAt = now;\n flipped.push(p.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function autoFailStuckRequestsV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.status === REQUEST_LIFECYCLE_V2.RUNNING &&\n r.startedAt &&\n now - r.startedAt >= _stateLlmV2.requestStuckMs\n ) {\n r.status = REQUEST_LIFECYCLE_V2.FAILED;\n r.lastSeenAt = now;\n if (!r.settledAt) r.settledAt = now;\n flipped.push(r.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function getLlmProvidersStatsV2() {\n const profilesByStatus = {};\n for (const s of Object.values(PROVIDER_MATURITY_V2)) profilesByStatus[s] = 0;\n for (const p of _stateLlmV2.profiles.values()) profilesByStatus[p.status]++;\n const requestsByStatus = {};\n for (const s of Object.values(REQUEST_LIFECYCLE_V2)) requestsByStatus[s] = 0;\n for (const r of _stateLlmV2.requests.values()) requestsByStatus[r.status]++;\n return {\n totalProfilesV2: _stateLlmV2.profiles.size,\n totalRequestsV2: _stateLlmV2.requests.size,\n maxActiveProfilesPerOwner: _stateLlmV2.maxActiveProfilesPerOwner,\n maxPendingRequestsPerProfile: _stateLlmV2.maxPendingRequestsPerProfile,\n profileIdleMs: _stateLlmV2.profileIdleMs,\n requestStuckMs: _stateLlmV2.requestStuckMs,\n profilesByStatus,\n requestsByStatus,\n };\n}\n\nexport function _resetStateLlmProvidersV2() {\n _stateLlmV2.profiles.clear();\n _stateLlmV2.requests.clear();\n _stateLlmV2.maxActiveProfilesPerOwner = PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER;\n _stateLlmV2.maxPendingRequestsPerProfile =\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE;\n _stateLlmV2.profileIdleMs = PROVIDER_DEFAULT_PROFILE_IDLE_MS;\n _stateLlmV2.requestStuckMs = PROVIDER_DEFAULT_REQUEST_STUCK_MS;\n}\n\n// =====================================================================\n// llm-providers V2 governance overlay (iter22)\n// =====================================================================\nexport const LLMGOV_PROFILE_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n DEGRADED: \"degraded\",\n ARCHIVED: \"archived\",\n});\nexport const LLMGOV_COMPLETION_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n INFERRING: \"inferring\",\n INFERRED: \"inferred\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\nconst _llmgovPTrans = new Map([\n [\n LLMGOV_PROFILE_MATURITY_V2.PENDING,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [LLMGOV_PROFILE_MATURITY_V2.ARCHIVED, new Set()],\n]);\nconst _llmgovPTerminal = new Set([LLMGOV_PROFILE_MATURITY_V2.ARCHIVED]);\nconst _llmgovJTrans = new Map([\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _llmgovPsV2 = new Map();\nconst _llmgovJsV2 = new Map();\nlet _llmgovMaxActive = 8,\n _llmgovMaxPending = 25,\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000,\n _llmgovStuckMs = 60 * 1000;\nfunction _llmgovPos(n, label) {\n const v = Math.floor(Number(n));\n if (!Number.isFinite(v) || v <= 0)\n throw new Error(`${label} must be positive integer`);\n return v;\n}\nfunction _llmgovCheckP(from, to) {\n const a = _llmgovPTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov profile transition ${from} → ${to}`);\n}\nfunction _llmgovCheckJ(from, to) {\n const a = _llmgovJTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov completion transition ${from} → ${to}`);\n}\nfunction _llmgovCountActive(owner) {\n let c = 0;\n for (const p of _llmgovPsV2.values())\n if (p.owner === owner && p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE)\n c++;\n return c;\n}\nfunction _llmgovCountPending(profileId) {\n let c = 0;\n for (const j of _llmgovJsV2.values())\n if (\n j.profileId === profileId &&\n (j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED ||\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING)\n )\n c++;\n return c;\n}\nexport function setMaxActiveLlmgovProfilesPerOwnerV2(n) {\n _llmgovMaxActive = _llmgovPos(n, \"maxActiveLlmgovProfilesPerOwner\");\n}\nexport function getMaxActiveLlmgovProfilesPerOwnerV2() {\n return _llmgovMaxActive;\n}\nexport function setMaxPendingLlmgovCompletionsPerProfileV2(n) {\n _llmgovMaxPending = _llmgovPos(n, \"maxPendingLlmgovCompletionsPerProfile\");\n}\nexport function getMaxPendingLlmgovCompletionsPerProfileV2() {\n return _llmgovMaxPending;\n}\nexport function setLlmgovProfileIdleMsV2(n) {\n _llmgovIdleMs = _llmgovPos(n, \"llmgovProfileIdleMs\");\n}\nexport function getLlmgovProfileIdleMsV2() {\n return _llmgovIdleMs;\n}\nexport function setLlmgovCompletionStuckMsV2(n) {\n _llmgovStuckMs = _llmgovPos(n, \"llmgovCompletionStuckMs\");\n}\nexport function getLlmgovCompletionStuckMsV2() {\n return _llmgovStuckMs;\n}\nexport function _resetStateLlmProvidersGovV2() {\n _llmgovPsV2.clear();\n _llmgovJsV2.clear();\n _llmgovMaxActive = 8;\n _llmgovMaxPending = 25;\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000;\n _llmgovStuckMs = 60 * 1000;\n}\nexport function registerLlmgovProfileV2({\n id,\n owner,\n provider,\n metadata,\n} = {}) {\n if (!id || !owner) throw new Error(\"id and owner required\");\n if (_llmgovPsV2.has(id))\n throw new Error(`llmgov profile ${id} already exists`);\n const now = Date.now();\n const p = {\n id,\n owner,\n provider: provider || \"ollama\",\n status: LLMGOV_PROFILE_MATURITY_V2.PENDING,\n createdAt: now,\n updatedAt: now,\n lastTouchedAt: now,\n activatedAt: null,\n archivedAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovPsV2.set(id, p);\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function activateLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n const isInitial = p.status === LLMGOV_PROFILE_MATURITY_V2.PENDING;\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ACTIVE);\n if (isInitial && _llmgovCountActive(p.owner) >= _llmgovMaxActive)\n throw new Error(`max active llmgov profiles for owner ${p.owner} reached`);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ACTIVE;\n p.updatedAt = now;\n p.lastTouchedAt = now;\n if (!p.activatedAt) p.activatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function degradeLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.DEGRADED);\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = Date.now();\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function archiveLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ARCHIVED);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ARCHIVED;\n p.updatedAt = now;\n if (!p.archivedAt) p.archivedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function touchLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n if (_llmgovPTerminal.has(p.status))\n throw new Error(`cannot touch terminal llmgov profile ${id}`);\n const now = Date.now();\n p.lastTouchedAt = now;\n p.updatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function getLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) return null;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function listLlmgovProfilesV2() {\n return [..._llmgovPsV2.values()].map((p) => ({\n ...p,\n metadata: { ...p.metadata },\n }));\n}\nexport function createLlmgovCompletionV2({\n id,\n profileId,\n model,\n metadata,\n} = {}) {\n if (!id || !profileId) throw new Error(\"id and profileId required\");\n if (_llmgovJsV2.has(id))\n throw new Error(`llmgov completion ${id} already exists`);\n if (!_llmgovPsV2.has(profileId))\n throw new Error(`llmgov profile ${profileId} not found`);\n if (_llmgovCountPending(profileId) >= _llmgovMaxPending)\n throw new Error(\n `max pending llmgov completions for profile ${profileId} reached`,\n );\n const now = Date.now();\n const j = {\n id,\n profileId,\n model: model || \"\",\n status: LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n updatedAt: now,\n startedAt: null,\n settledAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovJsV2.set(id, j);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function inferringLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING;\n j.updatedAt = now;\n if (!j.startedAt) j.startedAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function completeCompletionLlmgovV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function failLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.failReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function cancelLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.cancelReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function getLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) return null;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function listLlmgovCompletionsV2() {\n return [..._llmgovJsV2.values()].map((j) => ({\n ...j,\n metadata: { ...j.metadata },\n }));\n}\nexport function autoDegradeIdleLlmgovProfilesV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const p of _llmgovPsV2.values())\n if (\n p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE &&\n t - p.lastTouchedAt >= _llmgovIdleMs\n ) {\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = t;\n flipped.push(p.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function autoFailStuckLlmgovCompletionsV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const j of _llmgovJsV2.values())\n if (\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING &&\n j.startedAt != null &&\n t - j.startedAt >= _llmgovStuckMs\n ) {\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = t;\n if (!j.settledAt) j.settledAt = t;\n j.metadata.failReason = \"auto-fail-stuck\";\n flipped.push(j.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function getLlmProvidersGovStatsV2() {\n const profilesByStatus = {};\n for (const v of Object.values(LLMGOV_PROFILE_MATURITY_V2))\n profilesByStatus[v] = 0;\n for (const p of _llmgovPsV2.values()) profilesByStatus[p.status]++;\n const completionsByStatus = {};\n for (const v of Object.values(LLMGOV_COMPLETION_LIFECYCLE_V2))\n completionsByStatus[v] = 0;\n for (const j of _llmgovJsV2.values()) completionsByStatus[j.status]++;\n return {\n totalLlmgovProfilesV2: _llmgovPsV2.size,\n totalLlmgovCompletionsV2: _llmgovJsV2.size,\n maxActiveLlmgovProfilesPerOwner: _llmgovMaxActive,\n maxPendingLlmgovCompletionsPerProfile: _llmgovMaxPending,\n llmgovProfileIdleMs: _llmgovIdleMs,\n llmgovCompletionStuckMs: _llmgovStuckMs,\n profilesByStatus,\n completionsByStatus,\n };\n}\n"}, "6d9201d6095a6963430b0835cbe21aa2dca062a2": {"t": 1785754074.1633666, "n": 34233, "h": "ab1d6fd773a7d486", "c": "/**\n * LLM Provider registry — supports multiple AI providers with a unified interface.\n */\n\n/**\n * Built-in provider definitions.\n */\nexport const BUILT_IN_PROVIDERS = {\n ollama: {\n name: \"ollama\",\n displayName: \"Ollama (Local)\",\n baseUrl: \"http://localhost:11434\",\n apiKeyEnv: null,\n models: [\n \"qwen2.5:7b\",\n \"qwen2.5:14b\",\n \"qwen2.5-coder:14b\",\n \"qwen2:7b\",\n \"llama3:8b\",\n \"mistral:7b\",\n \"codellama:7b\",\n ],\n free: true,\n },\n openai: {\n name: \"openai\",\n displayName: \"OpenAI\",\n baseUrl: \"https://api.openai.com/v1\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\", \"o1\"],\n free: false,\n },\n anthropic: {\n name: \"anthropic\",\n displayName: \"Anthropic\",\n baseUrl: \"https://api.anthropic.com/v1\",\n apiKeyEnv: \"ANTHROPIC_API_KEY\",\n models: [\n \"claude-opus-4-6\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5-20251001\",\n ],\n free: false,\n },\n deepseek: {\n name: \"deepseek\",\n displayName: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com/v1\",\n apiKeyEnv: \"DEEPSEEK_API_KEY\",\n models: [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"],\n free: false,\n },\n dashscope: {\n name: \"dashscope\",\n displayName: \"DashScope (Alibaba)\",\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n apiKeyEnv: \"DASHSCOPE_API_KEY\",\n models: [\"qwen-turbo\", \"qwen-plus\", \"qwen-max\"],\n free: false,\n },\n gemini: {\n name: \"gemini\",\n displayName: \"Google Gemini\",\n baseUrl: \"https://generativelanguage.googleapis.com/v1beta\",\n apiKeyEnv: \"GEMINI_API_KEY\",\n models: [\"gemini-2.0-flash\", \"gemini-2.0-pro\", \"gemini-1.5-flash\"],\n free: false,\n },\n mistral: {\n name: \"mistral\",\n displayName: \"Mistral AI\",\n baseUrl: \"https://api.mistral.ai/v1\",\n apiKeyEnv: \"MISTRAL_API_KEY\",\n models: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n ],\n free: false,\n },\n volcengine: {\n name: \"volcengine\",\n displayName: \"Volcengine (火山引擎/豆包)\",\n baseUrl: \"https://ark.cn-beijing.volces.com/api/v3\",\n apiKeyEnv: \"VOLCENGINE_API_KEY\",\n models: [\n \"doubao-seed-1-6-251015\",\n \"doubao-seed-1-6-flash-250828\",\n \"doubao-seed-1-6-lite-251015\",\n \"doubao-seed-code\",\n ],\n free: false,\n },\n kimi: {\n name: \"kimi\",\n displayName: \"Kimi (月之暗面)\",\n baseUrl: \"https://api.moonshot.cn/v1\",\n apiKeyEnv: \"MOONSHOT_API_KEY\",\n models: [\n \"moonshot-v1-auto\",\n \"moonshot-v1-8k\",\n \"moonshot-v1-32k\",\n \"moonshot-v1-128k\",\n ],\n free: false,\n },\n minimax: {\n name: \"minimax\",\n displayName: \"MiniMax (海螺AI)\",\n baseUrl: \"https://api.minimax.chat/v1\",\n apiKeyEnv: \"MINIMAX_API_KEY\",\n models: [\"MiniMax-Text-01\", \"abab6.5s-chat\", \"abab5.5-chat\"],\n free: false,\n },\n};\n\n// A connectivity probe (`testProvider`) sends a tiny \"Hi\" and expects a quick\n// reply, so it must fail fast — a probe that hangs forever defeats its purpose\n// (it backs `cc llm test`, provider selection, doctor). Bound it with a total\n// timeout; env-overridable via CC_PROVIDER_TEST_TIMEOUT_MS.\nexport const PROVIDER_TEST_TIMEOUT_MS =\n Number(process.env.CC_PROVIDER_TEST_TIMEOUT_MS) || 20000;\n\nasync function _fetchWithTimeout(\n url,\n opts = {},\n ms = PROVIDER_TEST_TIMEOUT_MS,\n) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n if (timer && typeof timer.unref === \"function\") timer.unref();\n try {\n return await fetch(url, { ...opts, signal: controller.signal });\n } catch (e) {\n if (controller.signal.aborted) {\n throw new Error(`provider test timed out after ${ms / 1000}s`);\n }\n throw e;\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Provider registry — manages available providers and active selection.\n */\nexport class LLMProviderRegistry {\n constructor(db) {\n this.db = db;\n this.providers = new Map();\n this._ensureTable();\n this._loadBuiltins();\n this._loadCustom();\n }\n\n _ensureTable() {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS llm_providers (\n name TEXT PRIMARY KEY,\n display_name TEXT NOT NULL,\n base_url TEXT NOT NULL,\n api_key_env TEXT,\n models TEXT DEFAULT '[]',\n is_active INTEGER DEFAULT 0,\n custom INTEGER DEFAULT 0,\n created_at TEXT DEFAULT (datetime('now'))\n )\n `);\n }\n\n _loadBuiltins() {\n for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {\n this.providers.set(name, { ...def, custom: false });\n }\n }\n\n _loadCustom() {\n const rows = this.db\n .prepare(\"SELECT * FROM llm_providers WHERE custom = 1\")\n .all();\n for (const row of rows) {\n this.providers.set(row.name, {\n name: row.name,\n displayName: row.display_name,\n baseUrl: row.base_url,\n apiKeyEnv: row.api_key_env,\n models: JSON.parse(row.models || \"[]\"),\n custom: true,\n free: false,\n });\n }\n }\n\n /**\n * List all providers.\n */\n list() {\n const result = [];\n for (const [name, provider] of this.providers) {\n const hasKey = provider.apiKeyEnv\n ? !!process.env[provider.apiKeyEnv]\n : true;\n result.push({\n name,\n displayName: provider.displayName,\n baseUrl: provider.baseUrl,\n models: provider.models,\n hasApiKey: hasKey,\n custom: provider.custom || false,\n free: provider.free || false,\n });\n }\n return result;\n }\n\n /**\n * Get a specific provider.\n */\n get(name) {\n return this.providers.get(name) || null;\n }\n\n /**\n * Add a custom provider.\n */\n addProvider(name, config) {\n const provider = {\n name,\n displayName: config.displayName || name,\n baseUrl: config.baseUrl,\n apiKeyEnv: config.apiKeyEnv || null,\n models: config.models || [],\n custom: true,\n free: config.free || false,\n };\n\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, custom) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n provider.displayName,\n provider.baseUrl,\n provider.apiKeyEnv,\n JSON.stringify(provider.models),\n );\n\n this.providers.set(name, provider);\n return provider;\n }\n\n /**\n * Remove a custom provider.\n */\n removeProvider(name) {\n const provider = this.providers.get(name);\n if (!provider) return false;\n if (!provider.custom)\n throw new Error(`Cannot remove built-in provider \"${name}\"`);\n\n this.db\n .prepare(\"DELETE FROM llm_providers WHERE name = ? AND custom = 1\")\n .run(name);\n this.providers.delete(name);\n return true;\n }\n\n /**\n * Get/set the active provider.\n */\n getActive() {\n const row = this.db\n .prepare(\"SELECT name FROM llm_providers WHERE is_active = 1\")\n .get();\n return row ? row.name : \"ollama\";\n }\n\n setActive(name) {\n if (!this.providers.has(name)) {\n throw new Error(`Provider \"${name}\" not found`);\n }\n // Reset all\n this.db.prepare(\"UPDATE llm_providers SET is_active = 0 WHERE 1=1\").run();\n // Set active\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, is_active) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n this.providers.get(name).displayName,\n this.providers.get(name).baseUrl,\n this.providers.get(name).apiKeyEnv,\n JSON.stringify(this.providers.get(name).models),\n );\n return this.providers.get(name);\n }\n\n /**\n * Get API key for a provider (from env or config).\n */\n getApiKey(name) {\n const provider = this.providers.get(name);\n if (!provider) return null;\n if (!provider.apiKeyEnv) return null;\n return process.env[provider.apiKeyEnv] || null;\n }\n\n /**\n * Test provider connectivity.\n */\n async testProvider(name, model) {\n const provider = this.providers.get(name);\n if (!provider) throw new Error(`Provider \"${name}\" not found`);\n\n const start = Date.now();\n const testModel = model || provider.models[0];\n\n if (name === \"ollama\") {\n const res = await _fetchWithTimeout(`${provider.baseUrl}/api/generate`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: testModel, prompt: \"Hi\", stream: false }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n return {\n ok: true,\n elapsed: Date.now() - start,\n response: data.response?.trim(),\n };\n }\n\n if (name === \"gemini\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"GEMINI_API_KEY not set\");\n const res = await _fetchWithTimeout(\n `${provider.baseUrl}/models/${testModel}:generateContent?key=${key}`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ contents: [{ parts: [{ text: \"Hi\" }] }] }),\n },\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n if (name === \"anthropic\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"ANTHROPIC_API_KEY not set\");\n const res = await _fetchWithTimeout(`${provider.baseUrl}/messages`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": key,\n \"anthropic-version\": \"2023-06-01\",\n },\n body: JSON.stringify({\n model: testModel,\n max_tokens: 10,\n messages: [{ role: \"user\", content: \"Hi\" }],\n }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.content?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n // OpenAI-compatible (openai, deepseek, dashscope, mistral, volcengine, kimi, minimax)\n const key = this.getApiKey(name);\n if (!key) throw new Error(`${provider.apiKeyEnv} not set`);\n const res = await _fetchWithTimeout(\n `${provider.baseUrl}/chat/completions`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${key}`,\n },\n body: JSON.stringify({\n model: testModel,\n messages: [{ role: \"user\", content: \"Hi\" }],\n max_tokens: 10,\n }),\n },\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n}\n\n/* ─────────────────────────────────────────────────────────────────\n * V2 Governance Layer (in-memory, independent of SQLite llm_providers)\n *\n * Provider profile maturity: pending → active → suspended → retired\n * - retired terminal\n * - suspended → active recovery (cap-exempt)\n *\n * Request lifecycle: queued → running → completed | failed | cancelled\n * - 3 terminals\n * - per-profile pending-request cap counts queued+running\n *\n * Per-owner active-profile cap on pending→active only (recovery exempt).\n * Per-profile pending-request cap enforced at createRequestV2.\n *\n * Auto-flip:\n * - autoSuspendIdleProfilesV2 active w/ lastSeenAt past idle threshold → suspended\n * - autoFailStuckRequestsV2 running w/ startedAt past stuck threshold → failed\n * ───────────────────────────────────────────────────────────────── */\n\nexport const PROVIDER_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n SUSPENDED: \"suspended\",\n RETIRED: \"retired\",\n});\n\nexport const REQUEST_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n RUNNING: \"running\",\n COMPLETED: \"completed\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\n\nconst _PROVIDER_TRANSITIONS_V2 = new Map([\n [\n PROVIDER_MATURITY_V2.PENDING,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.ACTIVE,\n new Set([PROVIDER_MATURITY_V2.SUSPENDED, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.SUSPENDED,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [PROVIDER_MATURITY_V2.RETIRED, new Set()],\n]);\nconst _PROVIDER_TERMINALS_V2 = new Set([PROVIDER_MATURITY_V2.RETIRED]);\n\nconst _REQUEST_TRANSITIONS_V2 = new Map([\n [\n REQUEST_LIFECYCLE_V2.QUEUED,\n new Set([REQUEST_LIFECYCLE_V2.RUNNING, REQUEST_LIFECYCLE_V2.CANCELLED]),\n ],\n [\n REQUEST_LIFECYCLE_V2.RUNNING,\n new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [REQUEST_LIFECYCLE_V2.COMPLETED, new Set()],\n [REQUEST_LIFECYCLE_V2.FAILED, new Set()],\n [REQUEST_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _REQUEST_TERMINALS_V2 = new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n]);\n\nexport const PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER = 8;\nexport const PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE = 16;\nexport const PROVIDER_DEFAULT_PROFILE_IDLE_MS = 14 * 24 * 60 * 60 * 1000;\nexport const PROVIDER_DEFAULT_REQUEST_STUCK_MS = 5 * 60 * 1000;\n\nconst _stateLlmV2 = {\n profiles: new Map(),\n requests: new Map(),\n maxActiveProfilesPerOwner: PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER,\n maxPendingRequestsPerProfile:\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE,\n profileIdleMs: PROVIDER_DEFAULT_PROFILE_IDLE_MS,\n requestStuckMs: PROVIDER_DEFAULT_REQUEST_STUCK_MS,\n};\n\nfunction _posIntLlmV2(n, label) {\n const v = Math.floor(n);\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`${label} must be a positive integer`);\n }\n return v;\n}\n\nfunction _copyProfileV2(p) {\n return { ...p, metadata: { ...p.metadata } };\n}\n\nfunction _copyRequestV2(r) {\n return { ...r, metadata: { ...r.metadata } };\n}\n\nexport function getMaxActiveProfilesPerOwnerV2() {\n return _stateLlmV2.maxActiveProfilesPerOwner;\n}\n\nexport function setMaxActiveProfilesPerOwnerV2(n) {\n _stateLlmV2.maxActiveProfilesPerOwner = _posIntLlmV2(\n n,\n \"maxActiveProfilesPerOwner\",\n );\n}\n\nexport function getMaxPendingRequestsPerProfileV2() {\n return _stateLlmV2.maxPendingRequestsPerProfile;\n}\n\nexport function setMaxPendingRequestsPerProfileV2(n) {\n _stateLlmV2.maxPendingRequestsPerProfile = _posIntLlmV2(\n n,\n \"maxPendingRequestsPerProfile\",\n );\n}\n\nexport function getProfileIdleMsV2() {\n return _stateLlmV2.profileIdleMs;\n}\n\nexport function setProfileIdleMsV2(ms) {\n _stateLlmV2.profileIdleMs = _posIntLlmV2(ms, \"profileIdleMs\");\n}\n\nexport function getRequestStuckMsV2() {\n return _stateLlmV2.requestStuckMs;\n}\n\nexport function setRequestStuckMsV2(ms) {\n _stateLlmV2.requestStuckMs = _posIntLlmV2(ms, \"requestStuckMs\");\n}\n\nexport function getActiveProfileCountV2(ownerId) {\n let count = 0;\n for (const p of _stateLlmV2.profiles.values()) {\n if (p.ownerId === ownerId && p.status === PROVIDER_MATURITY_V2.ACTIVE) {\n count++;\n }\n }\n return count;\n}\n\nexport function getPendingRequestCountV2(profileId) {\n let count = 0;\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.profileId === profileId &&\n (r.status === REQUEST_LIFECYCLE_V2.QUEUED ||\n r.status === REQUEST_LIFECYCLE_V2.RUNNING)\n ) {\n count++;\n }\n }\n return count;\n}\n\nexport function registerProfileV2(\n id,\n { ownerId, provider, model, metadata } = {},\n) {\n if (!id) throw new Error(\"profile id is required\");\n if (!ownerId) throw new Error(\"ownerId is required\");\n if (!provider) throw new Error(\"provider is required\");\n if (_stateLlmV2.profiles.has(id))\n throw new Error(`profile ${id} already exists`);\n const now = Date.now();\n const profile = {\n id,\n ownerId,\n provider,\n model: model || \"default\",\n status: PROVIDER_MATURITY_V2.PENDING,\n createdAt: now,\n lastSeenAt: now,\n activatedAt: null,\n retiredAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.profiles.set(id, profile);\n return _copyProfileV2(profile);\n}\n\nexport function getProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n return p ? _copyProfileV2(p) : null;\n}\n\nexport function listProfilesV2({ ownerId, status, provider } = {}) {\n const out = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (ownerId && p.ownerId !== ownerId) continue;\n if (status && p.status !== status) continue;\n if (provider && p.provider !== provider) continue;\n out.push(_copyProfileV2(p));\n }\n return out;\n}\n\nexport function setProfileStatusV2(id, next) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n const allowed = _PROVIDER_TRANSITIONS_V2.get(p.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid profile transition: ${p.status} → ${next}`);\n }\n if (\n p.status === PROVIDER_MATURITY_V2.PENDING &&\n next === PROVIDER_MATURITY_V2.ACTIVE\n ) {\n const count = getActiveProfileCountV2(p.ownerId);\n if (count >= _stateLlmV2.maxActiveProfilesPerOwner) {\n throw new Error(\n `owner ${p.ownerId} active-profile cap reached (${count}/${_stateLlmV2.maxActiveProfilesPerOwner})`,\n );\n }\n }\n const now = Date.now();\n p.status = next;\n p.lastSeenAt = now;\n if (next === PROVIDER_MATURITY_V2.ACTIVE && !p.activatedAt)\n p.activatedAt = now;\n if (_PROVIDER_TERMINALS_V2.has(next) && !p.retiredAt) p.retiredAt = now;\n return _copyProfileV2(p);\n}\n\nexport function activateProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.ACTIVE);\n}\n\nexport function suspendProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.SUSPENDED);\n}\n\nexport function retireProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.RETIRED);\n}\n\nexport function touchProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n p.lastSeenAt = Date.now();\n return _copyProfileV2(p);\n}\n\nexport function createRequestV2(id, { profileId, kind, metadata } = {}) {\n if (!id) throw new Error(\"request id is required\");\n if (!profileId) throw new Error(\"profileId is required\");\n if (_stateLlmV2.requests.has(id))\n throw new Error(`request ${id} already exists`);\n const profile = _stateLlmV2.profiles.get(profileId);\n if (!profile) throw new Error(`profile ${profileId} not found`);\n const pending = getPendingRequestCountV2(profileId);\n if (pending >= _stateLlmV2.maxPendingRequestsPerProfile) {\n throw new Error(\n `profile ${profileId} pending-request cap reached (${pending}/${_stateLlmV2.maxPendingRequestsPerProfile})`,\n );\n }\n const now = Date.now();\n const req = {\n id,\n profileId,\n kind: kind || \"completion\",\n status: REQUEST_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n lastSeenAt: now,\n startedAt: null,\n settledAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.requests.set(id, req);\n return _copyRequestV2(req);\n}\n\nexport function getRequestV2(id) {\n const r = _stateLlmV2.requests.get(id);\n return r ? _copyRequestV2(r) : null;\n}\n\nexport function listRequestsV2({ profileId, status } = {}) {\n const out = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (profileId && r.profileId !== profileId) continue;\n if (status && r.status !== status) continue;\n out.push(_copyRequestV2(r));\n }\n return out;\n}\n\nexport function setRequestStatusV2(id, next) {\n const r = _stateLlmV2.requests.get(id);\n if (!r) throw new Error(`request ${id} not found`);\n const allowed = _REQUEST_TRANSITIONS_V2.get(r.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid request transition: ${r.status} → ${next}`);\n }\n const now = Date.now();\n r.status = next;\n r.lastSeenAt = now;\n if (next === REQUEST_LIFECYCLE_V2.RUNNING && !r.startedAt) r.startedAt = now;\n if (_REQUEST_TERMINALS_V2.has(next) && !r.settledAt) r.settledAt = now;\n return _copyRequestV2(r);\n}\n\nexport function startRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.RUNNING);\n}\n\nexport function completeRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.COMPLETED);\n}\n\nexport function failRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.FAILED);\n}\n\nexport function cancelRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.CANCELLED);\n}\n\nexport function autoSuspendIdleProfilesV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (\n p.status === PROVIDER_MATURITY_V2.ACTIVE &&\n now - p.lastSeenAt >= _stateLlmV2.profileIdleMs\n ) {\n p.status = PROVIDER_MATURITY_V2.SUSPENDED;\n p.lastSeenAt = now;\n flipped.push(p.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function autoFailStuckRequestsV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.status === REQUEST_LIFECYCLE_V2.RUNNING &&\n r.startedAt &&\n now - r.startedAt >= _stateLlmV2.requestStuckMs\n ) {\n r.status = REQUEST_LIFECYCLE_V2.FAILED;\n r.lastSeenAt = now;\n if (!r.settledAt) r.settledAt = now;\n flipped.push(r.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function getLlmProvidersStatsV2() {\n const profilesByStatus = {};\n for (const s of Object.values(PROVIDER_MATURITY_V2)) profilesByStatus[s] = 0;\n for (const p of _stateLlmV2.profiles.values()) profilesByStatus[p.status]++;\n const requestsByStatus = {};\n for (const s of Object.values(REQUEST_LIFECYCLE_V2)) requestsByStatus[s] = 0;\n for (const r of _stateLlmV2.requests.values()) requestsByStatus[r.status]++;\n return {\n totalProfilesV2: _stateLlmV2.profiles.size,\n totalRequestsV2: _stateLlmV2.requests.size,\n maxActiveProfilesPerOwner: _stateLlmV2.maxActiveProfilesPerOwner,\n maxPendingRequestsPerProfile: _stateLlmV2.maxPendingRequestsPerProfile,\n profileIdleMs: _stateLlmV2.profileIdleMs,\n requestStuckMs: _stateLlmV2.requestStuckMs,\n profilesByStatus,\n requestsByStatus,\n };\n}\n\nexport function _resetStateLlmProvidersV2() {\n _stateLlmV2.profiles.clear();\n _stateLlmV2.requests.clear();\n _stateLlmV2.maxActiveProfilesPerOwner = PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER;\n _stateLlmV2.maxPendingRequestsPerProfile =\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE;\n _stateLlmV2.profileIdleMs = PROVIDER_DEFAULT_PROFILE_IDLE_MS;\n _stateLlmV2.requestStuckMs = PROVIDER_DEFAULT_REQUEST_STUCK_MS;\n}\n\n// =====================================================================\n// llm-providers V2 governance overlay (iter22)\n// =====================================================================\nexport const LLMGOV_PROFILE_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n DEGRADED: \"degraded\",\n ARCHIVED: \"archived\",\n});\nexport const LLMGOV_COMPLETION_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n INFERRING: \"inferring\",\n INFERRED: \"inferred\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\nconst _llmgovPTrans = new Map([\n [\n LLMGOV_PROFILE_MATURITY_V2.PENDING,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [LLMGOV_PROFILE_MATURITY_V2.ARCHIVED, new Set()],\n]);\nconst _llmgovPTerminal = new Set([LLMGOV_PROFILE_MATURITY_V2.ARCHIVED]);\nconst _llmgovJTrans = new Map([\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _llmgovPsV2 = new Map();\nconst _llmgovJsV2 = new Map();\nlet _llmgovMaxActive = 8,\n _llmgovMaxPending = 25,\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000,\n _llmgovStuckMs = 60 * 1000;\nfunction _llmgovPos(n, label) {\n const v = Math.floor(Number(n));\n if (!Number.isFinite(v) || v <= 0)\n throw new Error(`${label} must be positive integer`);\n return v;\n}\nfunction _llmgovCheckP(from, to) {\n const a = _llmgovPTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov profile transition ${from} → ${to}`);\n}\nfunction _llmgovCheckJ(from, to) {\n const a = _llmgovJTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov completion transition ${from} → ${to}`);\n}\nfunction _llmgovCountActive(owner) {\n let c = 0;\n for (const p of _llmgovPsV2.values())\n if (p.owner === owner && p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE)\n c++;\n return c;\n}\nfunction _llmgovCountPending(profileId) {\n let c = 0;\n for (const j of _llmgovJsV2.values())\n if (\n j.profileId === profileId &&\n (j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED ||\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING)\n )\n c++;\n return c;\n}\nexport function setMaxActiveLlmgovProfilesPerOwnerV2(n) {\n _llmgovMaxActive = _llmgovPos(n, \"maxActiveLlmgovProfilesPerOwner\");\n}\nexport function getMaxActiveLlmgovProfilesPerOwnerV2() {\n return _llmgovMaxActive;\n}\nexport function setMaxPendingLlmgovCompletionsPerProfileV2(n) {\n _llmgovMaxPending = _llmgovPos(n, \"maxPendingLlmgovCompletionsPerProfile\");\n}\nexport function getMaxPendingLlmgovCompletionsPerProfileV2() {\n return _llmgovMaxPending;\n}\nexport function setLlmgovProfileIdleMsV2(n) {\n _llmgovIdleMs = _llmgovPos(n, \"llmgovProfileIdleMs\");\n}\nexport function getLlmgovProfileIdleMsV2() {\n return _llmgovIdleMs;\n}\nexport function setLlmgovCompletionStuckMsV2(n) {\n _llmgovStuckMs = _llmgovPos(n, \"llmgovCompletionStuckMs\");\n}\nexport function getLlmgovCompletionStuckMsV2() {\n return _llmgovStuckMs;\n}\nexport function _resetStateLlmProvidersGovV2() {\n _llmgovPsV2.clear();\n _llmgovJsV2.clear();\n _llmgovMaxActive = 8;\n _llmgovMaxPending = 25;\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000;\n _llmgovStuckMs = 60 * 1000;\n}\nexport function registerLlmgovProfileV2({\n id,\n owner,\n provider,\n metadata,\n} = {}) {\n if (!id || !owner) throw new Error(\"id and owner required\");\n if (_llmgovPsV2.has(id))\n throw new Error(`llmgov profile ${id} already exists`);\n const now = Date.now();\n const p = {\n id,\n owner,\n provider: provider || \"ollama\",\n status: LLMGOV_PROFILE_MATURITY_V2.PENDING,\n createdAt: now,\n updatedAt: now,\n lastTouchedAt: now,\n activatedAt: null,\n archivedAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovPsV2.set(id, p);\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function activateLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n const isInitial = p.status === LLMGOV_PROFILE_MATURITY_V2.PENDING;\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ACTIVE);\n if (isInitial && _llmgovCountActive(p.owner) >= _llmgovMaxActive)\n throw new Error(`max active llmgov profiles for owner ${p.owner} reached`);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ACTIVE;\n p.updatedAt = now;\n p.lastTouchedAt = now;\n if (!p.activatedAt) p.activatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function degradeLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.DEGRADED);\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = Date.now();\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function archiveLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ARCHIVED);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ARCHIVED;\n p.updatedAt = now;\n if (!p.archivedAt) p.archivedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function touchLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n if (_llmgovPTerminal.has(p.status))\n throw new Error(`cannot touch terminal llmgov profile ${id}`);\n const now = Date.now();\n p.lastTouchedAt = now;\n p.updatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function getLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) return null;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function listLlmgovProfilesV2() {\n return [..._llmgovPsV2.values()].map((p) => ({\n ...p,\n metadata: { ...p.metadata },\n }));\n}\nexport function createLlmgovCompletionV2({\n id,\n profileId,\n model,\n metadata,\n} = {}) {\n if (!id || !profileId) throw new Error(\"id and profileId required\");\n if (_llmgovJsV2.has(id))\n throw new Error(`llmgov completion ${id} already exists`);\n if (!_llmgovPsV2.has(profileId))\n throw new Error(`llmgov profile ${profileId} not found`);\n if (_llmgovCountPending(profileId) >= _llmgovMaxPending)\n throw new Error(\n `max pending llmgov completions for profile ${profileId} reached`,\n );\n const now = Date.now();\n const j = {\n id,\n profileId,\n model: model || \"\",\n status: LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n updatedAt: now,\n startedAt: null,\n settledAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovJsV2.set(id, j);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function inferringLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING;\n j.updatedAt = now;\n if (!j.startedAt) j.startedAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function completeCompletionLlmgovV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function failLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.failReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function cancelLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.cancelReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function getLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) return null;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function listLlmgovCompletionsV2() {\n return [..._llmgovJsV2.values()].map((j) => ({\n ...j,\n metadata: { ...j.metadata },\n }));\n}\nexport function autoDegradeIdleLlmgovProfilesV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const p of _llmgovPsV2.values())\n if (\n p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE &&\n t - p.lastTouchedAt >= _llmgovIdleMs\n ) {\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = t;\n flipped.push(p.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function autoFailStuckLlmgovCompletionsV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const j of _llmgovJsV2.values())\n if (\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING &&\n j.startedAt != null &&\n t - j.startedAt >= _llmgovStuckMs\n ) {\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = t;\n if (!j.settledAt) j.settledAt = t;\n j.metadata.failReason = \"auto-fail-stuck\";\n flipped.push(j.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function getLlmProvidersGovStatsV2() {\n const profilesByStatus = {};\n for (const v of Object.values(LLMGOV_PROFILE_MATURITY_V2))\n profilesByStatus[v] = 0;\n for (const p of _llmgovPsV2.values()) profilesByStatus[p.status]++;\n const completionsByStatus = {};\n for (const v of Object.values(LLMGOV_COMPLETION_LIFECYCLE_V2))\n completionsByStatus[v] = 0;\n for (const j of _llmgovJsV2.values()) completionsByStatus[j.status]++;\n return {\n totalLlmgovProfilesV2: _llmgovPsV2.size,\n totalLlmgovCompletionsV2: _llmgovJsV2.size,\n maxActiveLlmgovProfilesPerOwner: _llmgovMaxActive,\n maxPendingLlmgovCompletionsPerProfile: _llmgovMaxPending,\n llmgovProfileIdleMs: _llmgovIdleMs,\n llmgovCompletionStuckMs: _llmgovStuckMs,\n profilesByStatus,\n completionsByStatus,\n };\n}\n"}, "3b0b2dc09dceaf104b65bdb7bdc4448fc55e53a5": {"t": 1785754075.5821006, "n": 33221, "h": "e54c9970ff92948c", "c": "/**\n * LLM Provider registry — supports multiple AI providers with a unified interface.\n */\n\n/**\n * Built-in provider definitions.\n */\nexport const BUILT_IN_PROVIDERS = {\n ollama: {\n name: \"ollama\",\n displayName: \"Ollama (Local)\",\n baseUrl: \"http://localhost:11434\",\n apiKeyEnv: null,\n models: [\n \"qwen2.5:7b\",\n \"qwen2.5:14b\",\n \"qwen2.5-coder:14b\",\n \"qwen2:7b\",\n \"llama3:8b\",\n \"mistral:7b\",\n \"codellama:7b\",\n ],\n free: true,\n },\n openai: {\n name: \"openai\",\n displayName: \"OpenAI\",\n baseUrl: \"https://api.openai.com/v1\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\", \"o1\"],\n free: false,\n },\n anthropic: {\n name: \"anthropic\",\n displayName: \"Anthropic\",\n baseUrl: \"https://api.anthropic.com/v1\",\n apiKeyEnv: \"ANTHROPIC_API_KEY\",\n models: [\n \"claude-opus-4-6\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5-20251001\",\n ],\n free: false,\n },\n deepseek: {\n name: \"deepseek\",\n displayName: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com/v1\",\n apiKeyEnv: \"DEEPSEEK_API_KEY\",\n models: [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"],\n free: false,\n },\n dashscope: {\n name: \"dashscope\",\n displayName: \"DashScope (Alibaba)\",\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n apiKeyEnv: \"DASHSCOPE_API_KEY\",\n models: [\"qwen-turbo\", \"qwen-plus\", \"qwen-max\"],\n free: false,\n },\n gemini: {\n name: \"gemini\",\n displayName: \"Google Gemini\",\n baseUrl: \"https://generativelanguage.googleapis.com/v1beta\",\n apiKeyEnv: \"GEMINI_API_KEY\",\n models: [\"gemini-2.0-flash\", \"gemini-2.0-pro\", \"gemini-1.5-flash\"],\n free: false,\n },\n mistral: {\n name: \"mistral\",\n displayName: \"Mistral AI\",\n baseUrl: \"https://api.mistral.ai/v1\",\n apiKeyEnv: \"MISTRAL_API_KEY\",\n models: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n ],\n free: false,\n },\n volcengine: {\n name: \"volcengine\",\n displayName: \"Volcengine (火山引擎/豆包)\",\n baseUrl: \"https://ark.cn-beijing.volces.com/api/v3\",\n apiKeyEnv: \"VOLCENGINE_API_KEY\",\n models: [\n \"doubao-seed-1-6-251015\",\n \"doubao-seed-1-6-flash-250828\",\n \"doubao-seed-1-6-lite-251015\",\n \"doubao-seed-code\",\n ],\n free: false,\n },\n kimi: {\n name: \"kimi\",\n displayName: \"Kimi (月之暗面)\",\n baseUrl: \"https://api.moonshot.cn/v1\",\n apiKeyEnv: \"MOONSHOT_API_KEY\",\n models: [\n \"moonshot-v1-auto\",\n \"moonshot-v1-8k\",\n \"moonshot-v1-32k\",\n \"moonshot-v1-128k\",\n ],\n free: false,\n },\n minimax: {\n name: \"minimax\",\n displayName: \"MiniMax (海螺AI)\",\n baseUrl: \"https://api.minimax.chat/v1\",\n apiKeyEnv: \"MINIMAX_API_KEY\",\n models: [\"MiniMax-Text-01\", \"abab6.5s-chat\", \"abab5.5-chat\"],\n free: false,\n },\n};\n\n/**\n * Provider registry — manages available providers and active selection.\n */\nexport class LLMProviderRegistry {\n constructor(db) {\n this.db = db;\n this.providers = new Map();\n this._ensureTable();\n this._loadBuiltins();\n this._loadCustom();\n }\n\n _ensureTable() {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS llm_providers (\n name TEXT PRIMARY KEY,\n display_name TEXT NOT NULL,\n base_url TEXT NOT NULL,\n api_key_env TEXT,\n models TEXT DEFAULT '[]',\n is_active INTEGER DEFAULT 0,\n custom INTEGER DEFAULT 0,\n created_at TEXT DEFAULT (datetime('now'))\n )\n `);\n }\n\n _loadBuiltins() {\n for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {\n this.providers.set(name, { ...def, custom: false });\n }\n }\n\n _loadCustom() {\n const rows = this.db\n .prepare(\"SELECT * FROM llm_providers WHERE custom = 1\")\n .all();\n for (const row of rows) {\n this.providers.set(row.name, {\n name: row.name,\n displayName: row.display_name,\n baseUrl: row.base_url,\n apiKeyEnv: row.api_key_env,\n models: JSON.parse(row.models || \"[]\"),\n custom: true,\n free: false,\n });\n }\n }\n\n /**\n * List all providers.\n */\n list() {\n const result = [];\n for (const [name, provider] of this.providers) {\n const hasKey = provider.apiKeyEnv\n ? !!process.env[provider.apiKeyEnv]\n : true;\n result.push({\n name,\n displayName: provider.displayName,\n baseUrl: provider.baseUrl,\n models: provider.models,\n hasApiKey: hasKey,\n custom: provider.custom || false,\n free: provider.free || false,\n });\n }\n return result;\n }\n\n /**\n * Get a specific provider.\n */\n get(name) {\n return this.providers.get(name) || null;\n }\n\n /**\n * Add a custom provider.\n */\n addProvider(name, config) {\n const provider = {\n name,\n displayName: config.displayName || name,\n baseUrl: config.baseUrl,\n apiKeyEnv: config.apiKeyEnv || null,\n models: config.models || [],\n custom: true,\n free: config.free || false,\n };\n\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, custom) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n provider.displayName,\n provider.baseUrl,\n provider.apiKeyEnv,\n JSON.stringify(provider.models),\n );\n\n this.providers.set(name, provider);\n return provider;\n }\n\n /**\n * Remove a custom provider.\n */\n removeProvider(name) {\n const provider = this.providers.get(name);\n if (!provider) return false;\n if (!provider.custom)\n throw new Error(`Cannot remove built-in provider \"${name}\"`);\n\n this.db\n .prepare(\"DELETE FROM llm_providers WHERE name = ? AND custom = 1\")\n .run(name);\n this.providers.delete(name);\n return true;\n }\n\n /**\n * Get/set the active provider.\n */\n getActive() {\n const row = this.db\n .prepare(\"SELECT name FROM llm_providers WHERE is_active = 1\")\n .get();\n return row ? row.name : \"ollama\";\n }\n\n setActive(name) {\n if (!this.providers.has(name)) {\n throw new Error(`Provider \"${name}\" not found`);\n }\n // Reset all\n this.db.prepare(\"UPDATE llm_providers SET is_active = 0 WHERE 1=1\").run();\n // Set active\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, is_active) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n this.providers.get(name).displayName,\n this.providers.get(name).baseUrl,\n this.providers.get(name).apiKeyEnv,\n JSON.stringify(this.providers.get(name).models),\n );\n return this.providers.get(name);\n }\n\n /**\n * Get API key for a provider (from env or config).\n */\n getApiKey(name) {\n const provider = this.providers.get(name);\n if (!provider) return null;\n if (!provider.apiKeyEnv) return null;\n return process.env[provider.apiKeyEnv] || null;\n }\n\n /**\n * Test provider connectivity.\n */\n async testProvider(name, model) {\n const provider = this.providers.get(name);\n if (!provider) throw new Error(`Provider \"${name}\" not found`);\n\n const start = Date.now();\n const testModel = model || provider.models[0];\n\n if (name === \"ollama\") {\n const res = await fetch(`${provider.baseUrl}/api/generate`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: testModel, prompt: \"Hi\", stream: false }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n return {\n ok: true,\n elapsed: Date.now() - start,\n response: data.response?.trim(),\n };\n }\n\n if (name === \"gemini\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"GEMINI_API_KEY not set\");\n const res = await fetch(\n `${provider.baseUrl}/models/${testModel}:generateContent?key=${key}`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ contents: [{ parts: [{ text: \"Hi\" }] }] }),\n },\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n if (name === \"anthropic\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"ANTHROPIC_API_KEY not set\");\n const res = await fetch(`${provider.baseUrl}/messages`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": key,\n \"anthropic-version\": \"2023-06-01\",\n },\n body: JSON.stringify({\n model: testModel,\n max_tokens: 10,\n messages: [{ role: \"user\", content: \"Hi\" }],\n }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.content?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n // OpenAI-compatible (openai, deepseek, dashscope, mistral, volcengine, kimi, minimax)\n const key = this.getApiKey(name);\n if (!key) throw new Error(`${provider.apiKeyEnv} not set`);\n const res = await fetch(`${provider.baseUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${key}`,\n },\n body: JSON.stringify({\n model: testModel,\n messages: [{ role: \"user\", content: \"Hi\" }],\n max_tokens: 10,\n }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n}\n\n/* ─────────────────────────────────────────────────────────────────\n * V2 Governance Layer (in-memory, independent of SQLite llm_providers)\n *\n * Provider profile maturity: pending → active → suspended → retired\n * - retired terminal\n * - suspended → active recovery (cap-exempt)\n *\n * Request lifecycle: queued → running → completed | failed | cancelled\n * - 3 terminals\n * - per-profile pending-request cap counts queued+running\n *\n * Per-owner active-profile cap on pending→active only (recovery exempt).\n * Per-profile pending-request cap enforced at createRequestV2.\n *\n * Auto-flip:\n * - autoSuspendIdleProfilesV2 active w/ lastSeenAt past idle threshold → suspended\n * - autoFailStuckRequestsV2 running w/ startedAt past stuck threshold → failed\n * ───────────────────────────────────────────────────────────────── */\n\nexport const PROVIDER_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n SUSPENDED: \"suspended\",\n RETIRED: \"retired\",\n});\n\nexport const REQUEST_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n RUNNING: \"running\",\n COMPLETED: \"completed\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\n\nconst _PROVIDER_TRANSITIONS_V2 = new Map([\n [\n PROVIDER_MATURITY_V2.PENDING,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.ACTIVE,\n new Set([PROVIDER_MATURITY_V2.SUSPENDED, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.SUSPENDED,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [PROVIDER_MATURITY_V2.RETIRED, new Set()],\n]);\nconst _PROVIDER_TERMINALS_V2 = new Set([PROVIDER_MATURITY_V2.RETIRED]);\n\nconst _REQUEST_TRANSITIONS_V2 = new Map([\n [\n REQUEST_LIFECYCLE_V2.QUEUED,\n new Set([REQUEST_LIFECYCLE_V2.RUNNING, REQUEST_LIFECYCLE_V2.CANCELLED]),\n ],\n [\n REQUEST_LIFECYCLE_V2.RUNNING,\n new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [REQUEST_LIFECYCLE_V2.COMPLETED, new Set()],\n [REQUEST_LIFECYCLE_V2.FAILED, new Set()],\n [REQUEST_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _REQUEST_TERMINALS_V2 = new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n]);\n\nexport const PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER = 8;\nexport const PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE = 16;\nexport const PROVIDER_DEFAULT_PROFILE_IDLE_MS = 14 * 24 * 60 * 60 * 1000;\nexport const PROVIDER_DEFAULT_REQUEST_STUCK_MS = 5 * 60 * 1000;\n\nconst _stateLlmV2 = {\n profiles: new Map(),\n requests: new Map(),\n maxActiveProfilesPerOwner: PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER,\n maxPendingRequestsPerProfile:\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE,\n profileIdleMs: PROVIDER_DEFAULT_PROFILE_IDLE_MS,\n requestStuckMs: PROVIDER_DEFAULT_REQUEST_STUCK_MS,\n};\n\nfunction _posIntLlmV2(n, label) {\n const v = Math.floor(n);\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`${label} must be a positive integer`);\n }\n return v;\n}\n\nfunction _copyProfileV2(p) {\n return { ...p, metadata: { ...p.metadata } };\n}\n\nfunction _copyRequestV2(r) {\n return { ...r, metadata: { ...r.metadata } };\n}\n\nexport function getMaxActiveProfilesPerOwnerV2() {\n return _stateLlmV2.maxActiveProfilesPerOwner;\n}\n\nexport function setMaxActiveProfilesPerOwnerV2(n) {\n _stateLlmV2.maxActiveProfilesPerOwner = _posIntLlmV2(\n n,\n \"maxActiveProfilesPerOwner\",\n );\n}\n\nexport function getMaxPendingRequestsPerProfileV2() {\n return _stateLlmV2.maxPendingRequestsPerProfile;\n}\n\nexport function setMaxPendingRequestsPerProfileV2(n) {\n _stateLlmV2.maxPendingRequestsPerProfile = _posIntLlmV2(\n n,\n \"maxPendingRequestsPerProfile\",\n );\n}\n\nexport function getProfileIdleMsV2() {\n return _stateLlmV2.profileIdleMs;\n}\n\nexport function setProfileIdleMsV2(ms) {\n _stateLlmV2.profileIdleMs = _posIntLlmV2(ms, \"profileIdleMs\");\n}\n\nexport function getRequestStuckMsV2() {\n return _stateLlmV2.requestStuckMs;\n}\n\nexport function setRequestStuckMsV2(ms) {\n _stateLlmV2.requestStuckMs = _posIntLlmV2(ms, \"requestStuckMs\");\n}\n\nexport function getActiveProfileCountV2(ownerId) {\n let count = 0;\n for (const p of _stateLlmV2.profiles.values()) {\n if (p.ownerId === ownerId && p.status === PROVIDER_MATURITY_V2.ACTIVE) {\n count++;\n }\n }\n return count;\n}\n\nexport function getPendingRequestCountV2(profileId) {\n let count = 0;\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.profileId === profileId &&\n (r.status === REQUEST_LIFECYCLE_V2.QUEUED ||\n r.status === REQUEST_LIFECYCLE_V2.RUNNING)\n ) {\n count++;\n }\n }\n return count;\n}\n\nexport function registerProfileV2(\n id,\n { ownerId, provider, model, metadata } = {},\n) {\n if (!id) throw new Error(\"profile id is required\");\n if (!ownerId) throw new Error(\"ownerId is required\");\n if (!provider) throw new Error(\"provider is required\");\n if (_stateLlmV2.profiles.has(id))\n throw new Error(`profile ${id} already exists`);\n const now = Date.now();\n const profile = {\n id,\n ownerId,\n provider,\n model: model || \"default\",\n status: PROVIDER_MATURITY_V2.PENDING,\n createdAt: now,\n lastSeenAt: now,\n activatedAt: null,\n retiredAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.profiles.set(id, profile);\n return _copyProfileV2(profile);\n}\n\nexport function getProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n return p ? _copyProfileV2(p) : null;\n}\n\nexport function listProfilesV2({ ownerId, status, provider } = {}) {\n const out = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (ownerId && p.ownerId !== ownerId) continue;\n if (status && p.status !== status) continue;\n if (provider && p.provider !== provider) continue;\n out.push(_copyProfileV2(p));\n }\n return out;\n}\n\nexport function setProfileStatusV2(id, next) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n const allowed = _PROVIDER_TRANSITIONS_V2.get(p.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid profile transition: ${p.status} → ${next}`);\n }\n if (\n p.status === PROVIDER_MATURITY_V2.PENDING &&\n next === PROVIDER_MATURITY_V2.ACTIVE\n ) {\n const count = getActiveProfileCountV2(p.ownerId);\n if (count >= _stateLlmV2.maxActiveProfilesPerOwner) {\n throw new Error(\n `owner ${p.ownerId} active-profile cap reached (${count}/${_stateLlmV2.maxActiveProfilesPerOwner})`,\n );\n }\n }\n const now = Date.now();\n p.status = next;\n p.lastSeenAt = now;\n if (next === PROVIDER_MATURITY_V2.ACTIVE && !p.activatedAt)\n p.activatedAt = now;\n if (_PROVIDER_TERMINALS_V2.has(next) && !p.retiredAt) p.retiredAt = now;\n return _copyProfileV2(p);\n}\n\nexport function activateProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.ACTIVE);\n}\n\nexport function suspendProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.SUSPENDED);\n}\n\nexport function retireProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.RETIRED);\n}\n\nexport function touchProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n p.lastSeenAt = Date.now();\n return _copyProfileV2(p);\n}\n\nexport function createRequestV2(id, { profileId, kind, metadata } = {}) {\n if (!id) throw new Error(\"request id is required\");\n if (!profileId) throw new Error(\"profileId is required\");\n if (_stateLlmV2.requests.has(id))\n throw new Error(`request ${id} already exists`);\n const profile = _stateLlmV2.profiles.get(profileId);\n if (!profile) throw new Error(`profile ${profileId} not found`);\n const pending = getPendingRequestCountV2(profileId);\n if (pending >= _stateLlmV2.maxPendingRequestsPerProfile) {\n throw new Error(\n `profile ${profileId} pending-request cap reached (${pending}/${_stateLlmV2.maxPendingRequestsPerProfile})`,\n );\n }\n const now = Date.now();\n const req = {\n id,\n profileId,\n kind: kind || \"completion\",\n status: REQUEST_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n lastSeenAt: now,\n startedAt: null,\n settledAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.requests.set(id, req);\n return _copyRequestV2(req);\n}\n\nexport function getRequestV2(id) {\n const r = _stateLlmV2.requests.get(id);\n return r ? _copyRequestV2(r) : null;\n}\n\nexport function listRequestsV2({ profileId, status } = {}) {\n const out = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (profileId && r.profileId !== profileId) continue;\n if (status && r.status !== status) continue;\n out.push(_copyRequestV2(r));\n }\n return out;\n}\n\nexport function setRequestStatusV2(id, next) {\n const r = _stateLlmV2.requests.get(id);\n if (!r) throw new Error(`request ${id} not found`);\n const allowed = _REQUEST_TRANSITIONS_V2.get(r.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid request transition: ${r.status} → ${next}`);\n }\n const now = Date.now();\n r.status = next;\n r.lastSeenAt = now;\n if (next === REQUEST_LIFECYCLE_V2.RUNNING && !r.startedAt) r.startedAt = now;\n if (_REQUEST_TERMINALS_V2.has(next) && !r.settledAt) r.settledAt = now;\n return _copyRequestV2(r);\n}\n\nexport function startRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.RUNNING);\n}\n\nexport function completeRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.COMPLETED);\n}\n\nexport function failRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.FAILED);\n}\n\nexport function cancelRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.CANCELLED);\n}\n\nexport function autoSuspendIdleProfilesV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (\n p.status === PROVIDER_MATURITY_V2.ACTIVE &&\n now - p.lastSeenAt >= _stateLlmV2.profileIdleMs\n ) {\n p.status = PROVIDER_MATURITY_V2.SUSPENDED;\n p.lastSeenAt = now;\n flipped.push(p.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function autoFailStuckRequestsV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.status === REQUEST_LIFECYCLE_V2.RUNNING &&\n r.startedAt &&\n now - r.startedAt >= _stateLlmV2.requestStuckMs\n ) {\n r.status = REQUEST_LIFECYCLE_V2.FAILED;\n r.lastSeenAt = now;\n if (!r.settledAt) r.settledAt = now;\n flipped.push(r.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function getLlmProvidersStatsV2() {\n const profilesByStatus = {};\n for (const s of Object.values(PROVIDER_MATURITY_V2)) profilesByStatus[s] = 0;\n for (const p of _stateLlmV2.profiles.values()) profilesByStatus[p.status]++;\n const requestsByStatus = {};\n for (const s of Object.values(REQUEST_LIFECYCLE_V2)) requestsByStatus[s] = 0;\n for (const r of _stateLlmV2.requests.values()) requestsByStatus[r.status]++;\n return {\n totalProfilesV2: _stateLlmV2.profiles.size,\n totalRequestsV2: _stateLlmV2.requests.size,\n maxActiveProfilesPerOwner: _stateLlmV2.maxActiveProfilesPerOwner,\n maxPendingRequestsPerProfile: _stateLlmV2.maxPendingRequestsPerProfile,\n profileIdleMs: _stateLlmV2.profileIdleMs,\n requestStuckMs: _stateLlmV2.requestStuckMs,\n profilesByStatus,\n requestsByStatus,\n };\n}\n\nexport function _resetStateLlmProvidersV2() {\n _stateLlmV2.profiles.clear();\n _stateLlmV2.requests.clear();\n _stateLlmV2.maxActiveProfilesPerOwner = PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER;\n _stateLlmV2.maxPendingRequestsPerProfile =\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE;\n _stateLlmV2.profileIdleMs = PROVIDER_DEFAULT_PROFILE_IDLE_MS;\n _stateLlmV2.requestStuckMs = PROVIDER_DEFAULT_REQUEST_STUCK_MS;\n}\n\n// =====================================================================\n// llm-providers V2 governance overlay (iter22)\n// =====================================================================\nexport const LLMGOV_PROFILE_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n DEGRADED: \"degraded\",\n ARCHIVED: \"archived\",\n});\nexport const LLMGOV_COMPLETION_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n INFERRING: \"inferring\",\n INFERRED: \"inferred\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\nconst _llmgovPTrans = new Map([\n [\n LLMGOV_PROFILE_MATURITY_V2.PENDING,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [LLMGOV_PROFILE_MATURITY_V2.ARCHIVED, new Set()],\n]);\nconst _llmgovPTerminal = new Set([LLMGOV_PROFILE_MATURITY_V2.ARCHIVED]);\nconst _llmgovJTrans = new Map([\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _llmgovPsV2 = new Map();\nconst _llmgovJsV2 = new Map();\nlet _llmgovMaxActive = 8,\n _llmgovMaxPending = 25,\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000,\n _llmgovStuckMs = 60 * 1000;\nfunction _llmgovPos(n, label) {\n const v = Math.floor(Number(n));\n if (!Number.isFinite(v) || v <= 0)\n throw new Error(`${label} must be positive integer`);\n return v;\n}\nfunction _llmgovCheckP(from, to) {\n const a = _llmgovPTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov profile transition ${from} → ${to}`);\n}\nfunction _llmgovCheckJ(from, to) {\n const a = _llmgovJTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov completion transition ${from} → ${to}`);\n}\nfunction _llmgovCountActive(owner) {\n let c = 0;\n for (const p of _llmgovPsV2.values())\n if (p.owner === owner && p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE)\n c++;\n return c;\n}\nfunction _llmgovCountPending(profileId) {\n let c = 0;\n for (const j of _llmgovJsV2.values())\n if (\n j.profileId === profileId &&\n (j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED ||\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING)\n )\n c++;\n return c;\n}\nexport function setMaxActiveLlmgovProfilesPerOwnerV2(n) {\n _llmgovMaxActive = _llmgovPos(n, \"maxActiveLlmgovProfilesPerOwner\");\n}\nexport function getMaxActiveLlmgovProfilesPerOwnerV2() {\n return _llmgovMaxActive;\n}\nexport function setMaxPendingLlmgovCompletionsPerProfileV2(n) {\n _llmgovMaxPending = _llmgovPos(n, \"maxPendingLlmgovCompletionsPerProfile\");\n}\nexport function getMaxPendingLlmgovCompletionsPerProfileV2() {\n return _llmgovMaxPending;\n}\nexport function setLlmgovProfileIdleMsV2(n) {\n _llmgovIdleMs = _llmgovPos(n, \"llmgovProfileIdleMs\");\n}\nexport function getLlmgovProfileIdleMsV2() {\n return _llmgovIdleMs;\n}\nexport function setLlmgovCompletionStuckMsV2(n) {\n _llmgovStuckMs = _llmgovPos(n, \"llmgovCompletionStuckMs\");\n}\nexport function getLlmgovCompletionStuckMsV2() {\n return _llmgovStuckMs;\n}\nexport function _resetStateLlmProvidersGovV2() {\n _llmgovPsV2.clear();\n _llmgovJsV2.clear();\n _llmgovMaxActive = 8;\n _llmgovMaxPending = 25;\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000;\n _llmgovStuckMs = 60 * 1000;\n}\nexport function registerLlmgovProfileV2({\n id,\n owner,\n provider,\n metadata,\n} = {}) {\n if (!id || !owner) throw new Error(\"id and owner required\");\n if (_llmgovPsV2.has(id))\n throw new Error(`llmgov profile ${id} already exists`);\n const now = Date.now();\n const p = {\n id,\n owner,\n provider: provider || \"ollama\",\n status: LLMGOV_PROFILE_MATURITY_V2.PENDING,\n createdAt: now,\n updatedAt: now,\n lastTouchedAt: now,\n activatedAt: null,\n archivedAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovPsV2.set(id, p);\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function activateLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n const isInitial = p.status === LLMGOV_PROFILE_MATURITY_V2.PENDING;\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ACTIVE);\n if (isInitial && _llmgovCountActive(p.owner) >= _llmgovMaxActive)\n throw new Error(`max active llmgov profiles for owner ${p.owner} reached`);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ACTIVE;\n p.updatedAt = now;\n p.lastTouchedAt = now;\n if (!p.activatedAt) p.activatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function degradeLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.DEGRADED);\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = Date.now();\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function archiveLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ARCHIVED);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ARCHIVED;\n p.updatedAt = now;\n if (!p.archivedAt) p.archivedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function touchLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n if (_llmgovPTerminal.has(p.status))\n throw new Error(`cannot touch terminal llmgov profile ${id}`);\n const now = Date.now();\n p.lastTouchedAt = now;\n p.updatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function getLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) return null;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function listLlmgovProfilesV2() {\n return [..._llmgovPsV2.values()].map((p) => ({\n ...p,\n metadata: { ...p.metadata },\n }));\n}\nexport function createLlmgovCompletionV2({\n id,\n profileId,\n model,\n metadata,\n} = {}) {\n if (!id || !profileId) throw new Error(\"id and profileId required\");\n if (_llmgovJsV2.has(id))\n throw new Error(`llmgov completion ${id} already exists`);\n if (!_llmgovPsV2.has(profileId))\n throw new Error(`llmgov profile ${profileId} not found`);\n if (_llmgovCountPending(profileId) >= _llmgovMaxPending)\n throw new Error(\n `max pending llmgov completions for profile ${profileId} reached`,\n );\n const now = Date.now();\n const j = {\n id,\n profileId,\n model: model || \"\",\n status: LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n updatedAt: now,\n startedAt: null,\n settledAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovJsV2.set(id, j);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function inferringLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING;\n j.updatedAt = now;\n if (!j.startedAt) j.startedAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function completeCompletionLlmgovV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function failLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.failReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function cancelLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.cancelReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function getLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) return null;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function listLlmgovCompletionsV2() {\n return [..._llmgovJsV2.values()].map((j) => ({\n ...j,\n metadata: { ...j.metadata },\n }));\n}\nexport function autoDegradeIdleLlmgovProfilesV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const p of _llmgovPsV2.values())\n if (\n p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE &&\n t - p.lastTouchedAt >= _llmgovIdleMs\n ) {\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = t;\n flipped.push(p.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function autoFailStuckLlmgovCompletionsV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const j of _llmgovJsV2.values())\n if (\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING &&\n j.startedAt != null &&\n t - j.startedAt >= _llmgovStuckMs\n ) {\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = t;\n if (!j.settledAt) j.settledAt = t;\n j.metadata.failReason = \"auto-fail-stuck\";\n flipped.push(j.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function getLlmProvidersGovStatsV2() {\n const profilesByStatus = {};\n for (const v of Object.values(LLMGOV_PROFILE_MATURITY_V2))\n profilesByStatus[v] = 0;\n for (const p of _llmgovPsV2.values()) profilesByStatus[p.status]++;\n const completionsByStatus = {};\n for (const v of Object.values(LLMGOV_COMPLETION_LIFECYCLE_V2))\n completionsByStatus[v] = 0;\n for (const j of _llmgovJsV2.values()) completionsByStatus[j.status]++;\n return {\n totalLlmgovProfilesV2: _llmgovPsV2.size,\n totalLlmgovCompletionsV2: _llmgovJsV2.size,\n maxActiveLlmgovProfilesPerOwner: _llmgovMaxActive,\n maxPendingLlmgovCompletionsPerProfile: _llmgovMaxPending,\n llmgovProfileIdleMs: _llmgovIdleMs,\n llmgovCompletionStuckMs: _llmgovStuckMs,\n profilesByStatus,\n completionsByStatus,\n };\n}\n"}, "c3fe912fefd60bb6332748f59241fb6de61e9c5b": {"t": 1785754078.0781913, "n": 33221, "h": "e54c9970ff92948c", "c": "/**\n * LLM Provider registry — supports multiple AI providers with a unified interface.\n */\n\n/**\n * Built-in provider definitions.\n */\nexport const BUILT_IN_PROVIDERS = {\n ollama: {\n name: \"ollama\",\n displayName: \"Ollama (Local)\",\n baseUrl: \"http://localhost:11434\",\n apiKeyEnv: null,\n models: [\n \"qwen2.5:7b\",\n \"qwen2.5:14b\",\n \"qwen2.5-coder:14b\",\n \"qwen2:7b\",\n \"llama3:8b\",\n \"mistral:7b\",\n \"codellama:7b\",\n ],\n free: true,\n },\n openai: {\n name: \"openai\",\n displayName: \"OpenAI\",\n baseUrl: \"https://api.openai.com/v1\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\", \"o1\"],\n free: false,\n },\n anthropic: {\n name: \"anthropic\",\n displayName: \"Anthropic\",\n baseUrl: \"https://api.anthropic.com/v1\",\n apiKeyEnv: \"ANTHROPIC_API_KEY\",\n models: [\n \"claude-opus-4-6\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5-20251001\",\n ],\n free: false,\n },\n deepseek: {\n name: \"deepseek\",\n displayName: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com/v1\",\n apiKeyEnv: \"DEEPSEEK_API_KEY\",\n models: [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"],\n free: false,\n },\n dashscope: {\n name: \"dashscope\",\n displayName: \"DashScope (Alibaba)\",\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n apiKeyEnv: \"DASHSCOPE_API_KEY\",\n models: [\"qwen-turbo\", \"qwen-plus\", \"qwen-max\"],\n free: false,\n },\n gemini: {\n name: \"gemini\",\n displayName: \"Google Gemini\",\n baseUrl: \"https://generativelanguage.googleapis.com/v1beta\",\n apiKeyEnv: \"GEMINI_API_KEY\",\n models: [\"gemini-2.0-flash\", \"gemini-2.0-pro\", \"gemini-1.5-flash\"],\n free: false,\n },\n mistral: {\n name: \"mistral\",\n displayName: \"Mistral AI\",\n baseUrl: \"https://api.mistral.ai/v1\",\n apiKeyEnv: \"MISTRAL_API_KEY\",\n models: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n ],\n free: false,\n },\n volcengine: {\n name: \"volcengine\",\n displayName: \"Volcengine (火山引擎/豆包)\",\n baseUrl: \"https://ark.cn-beijing.volces.com/api/v3\",\n apiKeyEnv: \"VOLCENGINE_API_KEY\",\n models: [\n \"doubao-seed-1-6-251015\",\n \"doubao-seed-1-6-flash-250828\",\n \"doubao-seed-1-6-lite-251015\",\n \"doubao-seed-code\",\n ],\n free: false,\n },\n kimi: {\n name: \"kimi\",\n displayName: \"Kimi (月之暗面)\",\n baseUrl: \"https://api.moonshot.cn/v1\",\n apiKeyEnv: \"MOONSHOT_API_KEY\",\n models: [\n \"moonshot-v1-auto\",\n \"moonshot-v1-8k\",\n \"moonshot-v1-32k\",\n \"moonshot-v1-128k\",\n ],\n free: false,\n },\n minimax: {\n name: \"minimax\",\n displayName: \"MiniMax (海螺AI)\",\n baseUrl: \"https://api.minimax.chat/v1\",\n apiKeyEnv: \"MINIMAX_API_KEY\",\n models: [\"MiniMax-Text-01\", \"abab6.5s-chat\", \"abab5.5-chat\"],\n free: false,\n },\n};\n\n/**\n * Provider registry — manages available providers and active selection.\n */\nexport class LLMProviderRegistry {\n constructor(db) {\n this.db = db;\n this.providers = new Map();\n this._ensureTable();\n this._loadBuiltins();\n this._loadCustom();\n }\n\n _ensureTable() {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS llm_providers (\n name TEXT PRIMARY KEY,\n display_name TEXT NOT NULL,\n base_url TEXT NOT NULL,\n api_key_env TEXT,\n models TEXT DEFAULT '[]',\n is_active INTEGER DEFAULT 0,\n custom INTEGER DEFAULT 0,\n created_at TEXT DEFAULT (datetime('now'))\n )\n `);\n }\n\n _loadBuiltins() {\n for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {\n this.providers.set(name, { ...def, custom: false });\n }\n }\n\n _loadCustom() {\n const rows = this.db\n .prepare(\"SELECT * FROM llm_providers WHERE custom = 1\")\n .all();\n for (const row of rows) {\n this.providers.set(row.name, {\n name: row.name,\n displayName: row.display_name,\n baseUrl: row.base_url,\n apiKeyEnv: row.api_key_env,\n models: JSON.parse(row.models || \"[]\"),\n custom: true,\n free: false,\n });\n }\n }\n\n /**\n * List all providers.\n */\n list() {\n const result = [];\n for (const [name, provider] of this.providers) {\n const hasKey = provider.apiKeyEnv\n ? !!process.env[provider.apiKeyEnv]\n : true;\n result.push({\n name,\n displayName: provider.displayName,\n baseUrl: provider.baseUrl,\n models: provider.models,\n hasApiKey: hasKey,\n custom: provider.custom || false,\n free: provider.free || false,\n });\n }\n return result;\n }\n\n /**\n * Get a specific provider.\n */\n get(name) {\n return this.providers.get(name) || null;\n }\n\n /**\n * Add a custom provider.\n */\n addProvider(name, config) {\n const provider = {\n name,\n displayName: config.displayName || name,\n baseUrl: config.baseUrl,\n apiKeyEnv: config.apiKeyEnv || null,\n models: config.models || [],\n custom: true,\n free: config.free || false,\n };\n\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, custom) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n provider.displayName,\n provider.baseUrl,\n provider.apiKeyEnv,\n JSON.stringify(provider.models),\n );\n\n this.providers.set(name, provider);\n return provider;\n }\n\n /**\n * Remove a custom provider.\n */\n removeProvider(name) {\n const provider = this.providers.get(name);\n if (!provider) return false;\n if (!provider.custom)\n throw new Error(`Cannot remove built-in provider \"${name}\"`);\n\n this.db\n .prepare(\"DELETE FROM llm_providers WHERE name = ? AND custom = 1\")\n .run(name);\n this.providers.delete(name);\n return true;\n }\n\n /**\n * Get/set the active provider.\n */\n getActive() {\n const row = this.db\n .prepare(\"SELECT name FROM llm_providers WHERE is_active = 1\")\n .get();\n return row ? row.name : \"ollama\";\n }\n\n setActive(name) {\n if (!this.providers.has(name)) {\n throw new Error(`Provider \"${name}\" not found`);\n }\n // Reset all\n this.db.prepare(\"UPDATE llm_providers SET is_active = 0 WHERE 1=1\").run();\n // Set active\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, is_active) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n this.providers.get(name).displayName,\n this.providers.get(name).baseUrl,\n this.providers.get(name).apiKeyEnv,\n JSON.stringify(this.providers.get(name).models),\n );\n return this.providers.get(name);\n }\n\n /**\n * Get API key for a provider (from env or config).\n */\n getApiKey(name) {\n const provider = this.providers.get(name);\n if (!provider) return null;\n if (!provider.apiKeyEnv) return null;\n return process.env[provider.apiKeyEnv] || null;\n }\n\n /**\n * Test provider connectivity.\n */\n async testProvider(name, model) {\n const provider = this.providers.get(name);\n if (!provider) throw new Error(`Provider \"${name}\" not found`);\n\n const start = Date.now();\n const testModel = model || provider.models[0];\n\n if (name === \"ollama\") {\n const res = await fetch(`${provider.baseUrl}/api/generate`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: testModel, prompt: \"Hi\", stream: false }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n return {\n ok: true,\n elapsed: Date.now() - start,\n response: data.response?.trim(),\n };\n }\n\n if (name === \"gemini\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"GEMINI_API_KEY not set\");\n const res = await fetch(\n `${provider.baseUrl}/models/${testModel}:generateContent?key=${key}`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ contents: [{ parts: [{ text: \"Hi\" }] }] }),\n },\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n if (name === \"anthropic\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"ANTHROPIC_API_KEY not set\");\n const res = await fetch(`${provider.baseUrl}/messages`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": key,\n \"anthropic-version\": \"2023-06-01\",\n },\n body: JSON.stringify({\n model: testModel,\n max_tokens: 10,\n messages: [{ role: \"user\", content: \"Hi\" }],\n }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.content?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n // OpenAI-compatible (openai, deepseek, dashscope, mistral, volcengine, kimi, minimax)\n const key = this.getApiKey(name);\n if (!key) throw new Error(`${provider.apiKeyEnv} not set`);\n const res = await fetch(`${provider.baseUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${key}`,\n },\n body: JSON.stringify({\n model: testModel,\n messages: [{ role: \"user\", content: \"Hi\" }],\n max_tokens: 10,\n }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n}\n\n/* ─────────────────────────────────────────────────────────────────\n * V2 Governance Layer (in-memory, independent of SQLite llm_providers)\n *\n * Provider profile maturity: pending → active → suspended → retired\n * - retired terminal\n * - suspended → active recovery (cap-exempt)\n *\n * Request lifecycle: queued → running → completed | failed | cancelled\n * - 3 terminals\n * - per-profile pending-request cap counts queued+running\n *\n * Per-owner active-profile cap on pending→active only (recovery exempt).\n * Per-profile pending-request cap enforced at createRequestV2.\n *\n * Auto-flip:\n * - autoSuspendIdleProfilesV2 active w/ lastSeenAt past idle threshold → suspended\n * - autoFailStuckRequestsV2 running w/ startedAt past stuck threshold → failed\n * ───────────────────────────────────────────────────────────────── */\n\nexport const PROVIDER_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n SUSPENDED: \"suspended\",\n RETIRED: \"retired\",\n});\n\nexport const REQUEST_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n RUNNING: \"running\",\n COMPLETED: \"completed\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\n\nconst _PROVIDER_TRANSITIONS_V2 = new Map([\n [\n PROVIDER_MATURITY_V2.PENDING,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.ACTIVE,\n new Set([PROVIDER_MATURITY_V2.SUSPENDED, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.SUSPENDED,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [PROVIDER_MATURITY_V2.RETIRED, new Set()],\n]);\nconst _PROVIDER_TERMINALS_V2 = new Set([PROVIDER_MATURITY_V2.RETIRED]);\n\nconst _REQUEST_TRANSITIONS_V2 = new Map([\n [\n REQUEST_LIFECYCLE_V2.QUEUED,\n new Set([REQUEST_LIFECYCLE_V2.RUNNING, REQUEST_LIFECYCLE_V2.CANCELLED]),\n ],\n [\n REQUEST_LIFECYCLE_V2.RUNNING,\n new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [REQUEST_LIFECYCLE_V2.COMPLETED, new Set()],\n [REQUEST_LIFECYCLE_V2.FAILED, new Set()],\n [REQUEST_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _REQUEST_TERMINALS_V2 = new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n]);\n\nexport const PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER = 8;\nexport const PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE = 16;\nexport const PROVIDER_DEFAULT_PROFILE_IDLE_MS = 14 * 24 * 60 * 60 * 1000;\nexport const PROVIDER_DEFAULT_REQUEST_STUCK_MS = 5 * 60 * 1000;\n\nconst _stateLlmV2 = {\n profiles: new Map(),\n requests: new Map(),\n maxActiveProfilesPerOwner: PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER,\n maxPendingRequestsPerProfile:\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE,\n profileIdleMs: PROVIDER_DEFAULT_PROFILE_IDLE_MS,\n requestStuckMs: PROVIDER_DEFAULT_REQUEST_STUCK_MS,\n};\n\nfunction _posIntLlmV2(n, label) {\n const v = Math.floor(n);\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`${label} must be a positive integer`);\n }\n return v;\n}\n\nfunction _copyProfileV2(p) {\n return { ...p, metadata: { ...p.metadata } };\n}\n\nfunction _copyRequestV2(r) {\n return { ...r, metadata: { ...r.metadata } };\n}\n\nexport function getMaxActiveProfilesPerOwnerV2() {\n return _stateLlmV2.maxActiveProfilesPerOwner;\n}\n\nexport function setMaxActiveProfilesPerOwnerV2(n) {\n _stateLlmV2.maxActiveProfilesPerOwner = _posIntLlmV2(\n n,\n \"maxActiveProfilesPerOwner\",\n );\n}\n\nexport function getMaxPendingRequestsPerProfileV2() {\n return _stateLlmV2.maxPendingRequestsPerProfile;\n}\n\nexport function setMaxPendingRequestsPerProfileV2(n) {\n _stateLlmV2.maxPendingRequestsPerProfile = _posIntLlmV2(\n n,\n \"maxPendingRequestsPerProfile\",\n );\n}\n\nexport function getProfileIdleMsV2() {\n return _stateLlmV2.profileIdleMs;\n}\n\nexport function setProfileIdleMsV2(ms) {\n _stateLlmV2.profileIdleMs = _posIntLlmV2(ms, \"profileIdleMs\");\n}\n\nexport function getRequestStuckMsV2() {\n return _stateLlmV2.requestStuckMs;\n}\n\nexport function setRequestStuckMsV2(ms) {\n _stateLlmV2.requestStuckMs = _posIntLlmV2(ms, \"requestStuckMs\");\n}\n\nexport function getActiveProfileCountV2(ownerId) {\n let count = 0;\n for (const p of _stateLlmV2.profiles.values()) {\n if (p.ownerId === ownerId && p.status === PROVIDER_MATURITY_V2.ACTIVE) {\n count++;\n }\n }\n return count;\n}\n\nexport function getPendingRequestCountV2(profileId) {\n let count = 0;\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.profileId === profileId &&\n (r.status === REQUEST_LIFECYCLE_V2.QUEUED ||\n r.status === REQUEST_LIFECYCLE_V2.RUNNING)\n ) {\n count++;\n }\n }\n return count;\n}\n\nexport function registerProfileV2(\n id,\n { ownerId, provider, model, metadata } = {},\n) {\n if (!id) throw new Error(\"profile id is required\");\n if (!ownerId) throw new Error(\"ownerId is required\");\n if (!provider) throw new Error(\"provider is required\");\n if (_stateLlmV2.profiles.has(id))\n throw new Error(`profile ${id} already exists`);\n const now = Date.now();\n const profile = {\n id,\n ownerId,\n provider,\n model: model || \"default\",\n status: PROVIDER_MATURITY_V2.PENDING,\n createdAt: now,\n lastSeenAt: now,\n activatedAt: null,\n retiredAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.profiles.set(id, profile);\n return _copyProfileV2(profile);\n}\n\nexport function getProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n return p ? _copyProfileV2(p) : null;\n}\n\nexport function listProfilesV2({ ownerId, status, provider } = {}) {\n const out = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (ownerId && p.ownerId !== ownerId) continue;\n if (status && p.status !== status) continue;\n if (provider && p.provider !== provider) continue;\n out.push(_copyProfileV2(p));\n }\n return out;\n}\n\nexport function setProfileStatusV2(id, next) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n const allowed = _PROVIDER_TRANSITIONS_V2.get(p.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid profile transition: ${p.status} → ${next}`);\n }\n if (\n p.status === PROVIDER_MATURITY_V2.PENDING &&\n next === PROVIDER_MATURITY_V2.ACTIVE\n ) {\n const count = getActiveProfileCountV2(p.ownerId);\n if (count >= _stateLlmV2.maxActiveProfilesPerOwner) {\n throw new Error(\n `owner ${p.ownerId} active-profile cap reached (${count}/${_stateLlmV2.maxActiveProfilesPerOwner})`,\n );\n }\n }\n const now = Date.now();\n p.status = next;\n p.lastSeenAt = now;\n if (next === PROVIDER_MATURITY_V2.ACTIVE && !p.activatedAt)\n p.activatedAt = now;\n if (_PROVIDER_TERMINALS_V2.has(next) && !p.retiredAt) p.retiredAt = now;\n return _copyProfileV2(p);\n}\n\nexport function activateProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.ACTIVE);\n}\n\nexport function suspendProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.SUSPENDED);\n}\n\nexport function retireProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.RETIRED);\n}\n\nexport function touchProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n p.lastSeenAt = Date.now();\n return _copyProfileV2(p);\n}\n\nexport function createRequestV2(id, { profileId, kind, metadata } = {}) {\n if (!id) throw new Error(\"request id is required\");\n if (!profileId) throw new Error(\"profileId is required\");\n if (_stateLlmV2.requests.has(id))\n throw new Error(`request ${id} already exists`);\n const profile = _stateLlmV2.profiles.get(profileId);\n if (!profile) throw new Error(`profile ${profileId} not found`);\n const pending = getPendingRequestCountV2(profileId);\n if (pending >= _stateLlmV2.maxPendingRequestsPerProfile) {\n throw new Error(\n `profile ${profileId} pending-request cap reached (${pending}/${_stateLlmV2.maxPendingRequestsPerProfile})`,\n );\n }\n const now = Date.now();\n const req = {\n id,\n profileId,\n kind: kind || \"completion\",\n status: REQUEST_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n lastSeenAt: now,\n startedAt: null,\n settledAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.requests.set(id, req);\n return _copyRequestV2(req);\n}\n\nexport function getRequestV2(id) {\n const r = _stateLlmV2.requests.get(id);\n return r ? _copyRequestV2(r) : null;\n}\n\nexport function listRequestsV2({ profileId, status } = {}) {\n const out = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (profileId && r.profileId !== profileId) continue;\n if (status && r.status !== status) continue;\n out.push(_copyRequestV2(r));\n }\n return out;\n}\n\nexport function setRequestStatusV2(id, next) {\n const r = _stateLlmV2.requests.get(id);\n if (!r) throw new Error(`request ${id} not found`);\n const allowed = _REQUEST_TRANSITIONS_V2.get(r.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid request transition: ${r.status} → ${next}`);\n }\n const now = Date.now();\n r.status = next;\n r.lastSeenAt = now;\n if (next === REQUEST_LIFECYCLE_V2.RUNNING && !r.startedAt) r.startedAt = now;\n if (_REQUEST_TERMINALS_V2.has(next) && !r.settledAt) r.settledAt = now;\n return _copyRequestV2(r);\n}\n\nexport function startRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.RUNNING);\n}\n\nexport function completeRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.COMPLETED);\n}\n\nexport function failRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.FAILED);\n}\n\nexport function cancelRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.CANCELLED);\n}\n\nexport function autoSuspendIdleProfilesV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (\n p.status === PROVIDER_MATURITY_V2.ACTIVE &&\n now - p.lastSeenAt >= _stateLlmV2.profileIdleMs\n ) {\n p.status = PROVIDER_MATURITY_V2.SUSPENDED;\n p.lastSeenAt = now;\n flipped.push(p.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function autoFailStuckRequestsV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.status === REQUEST_LIFECYCLE_V2.RUNNING &&\n r.startedAt &&\n now - r.startedAt >= _stateLlmV2.requestStuckMs\n ) {\n r.status = REQUEST_LIFECYCLE_V2.FAILED;\n r.lastSeenAt = now;\n if (!r.settledAt) r.settledAt = now;\n flipped.push(r.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function getLlmProvidersStatsV2() {\n const profilesByStatus = {};\n for (const s of Object.values(PROVIDER_MATURITY_V2)) profilesByStatus[s] = 0;\n for (const p of _stateLlmV2.profiles.values()) profilesByStatus[p.status]++;\n const requestsByStatus = {};\n for (const s of Object.values(REQUEST_LIFECYCLE_V2)) requestsByStatus[s] = 0;\n for (const r of _stateLlmV2.requests.values()) requestsByStatus[r.status]++;\n return {\n totalProfilesV2: _stateLlmV2.profiles.size,\n totalRequestsV2: _stateLlmV2.requests.size,\n maxActiveProfilesPerOwner: _stateLlmV2.maxActiveProfilesPerOwner,\n maxPendingRequestsPerProfile: _stateLlmV2.maxPendingRequestsPerProfile,\n profileIdleMs: _stateLlmV2.profileIdleMs,\n requestStuckMs: _stateLlmV2.requestStuckMs,\n profilesByStatus,\n requestsByStatus,\n };\n}\n\nexport function _resetStateLlmProvidersV2() {\n _stateLlmV2.profiles.clear();\n _stateLlmV2.requests.clear();\n _stateLlmV2.maxActiveProfilesPerOwner = PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER;\n _stateLlmV2.maxPendingRequestsPerProfile =\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE;\n _stateLlmV2.profileIdleMs = PROVIDER_DEFAULT_PROFILE_IDLE_MS;\n _stateLlmV2.requestStuckMs = PROVIDER_DEFAULT_REQUEST_STUCK_MS;\n}\n\n// =====================================================================\n// llm-providers V2 governance overlay (iter22)\n// =====================================================================\nexport const LLMGOV_PROFILE_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n DEGRADED: \"degraded\",\n ARCHIVED: \"archived\",\n});\nexport const LLMGOV_COMPLETION_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n INFERRING: \"inferring\",\n INFERRED: \"inferred\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\nconst _llmgovPTrans = new Map([\n [\n LLMGOV_PROFILE_MATURITY_V2.PENDING,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [LLMGOV_PROFILE_MATURITY_V2.ARCHIVED, new Set()],\n]);\nconst _llmgovPTerminal = new Set([LLMGOV_PROFILE_MATURITY_V2.ARCHIVED]);\nconst _llmgovJTrans = new Map([\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _llmgovPsV2 = new Map();\nconst _llmgovJsV2 = new Map();\nlet _llmgovMaxActive = 8,\n _llmgovMaxPending = 25,\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000,\n _llmgovStuckMs = 60 * 1000;\nfunction _llmgovPos(n, label) {\n const v = Math.floor(Number(n));\n if (!Number.isFinite(v) || v <= 0)\n throw new Error(`${label} must be positive integer`);\n return v;\n}\nfunction _llmgovCheckP(from, to) {\n const a = _llmgovPTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov profile transition ${from} → ${to}`);\n}\nfunction _llmgovCheckJ(from, to) {\n const a = _llmgovJTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov completion transition ${from} → ${to}`);\n}\nfunction _llmgovCountActive(owner) {\n let c = 0;\n for (const p of _llmgovPsV2.values())\n if (p.owner === owner && p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE)\n c++;\n return c;\n}\nfunction _llmgovCountPending(profileId) {\n let c = 0;\n for (const j of _llmgovJsV2.values())\n if (\n j.profileId === profileId &&\n (j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED ||\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING)\n )\n c++;\n return c;\n}\nexport function setMaxActiveLlmgovProfilesPerOwnerV2(n) {\n _llmgovMaxActive = _llmgovPos(n, \"maxActiveLlmgovProfilesPerOwner\");\n}\nexport function getMaxActiveLlmgovProfilesPerOwnerV2() {\n return _llmgovMaxActive;\n}\nexport function setMaxPendingLlmgovCompletionsPerProfileV2(n) {\n _llmgovMaxPending = _llmgovPos(n, \"maxPendingLlmgovCompletionsPerProfile\");\n}\nexport function getMaxPendingLlmgovCompletionsPerProfileV2() {\n return _llmgovMaxPending;\n}\nexport function setLlmgovProfileIdleMsV2(n) {\n _llmgovIdleMs = _llmgovPos(n, \"llmgovProfileIdleMs\");\n}\nexport function getLlmgovProfileIdleMsV2() {\n return _llmgovIdleMs;\n}\nexport function setLlmgovCompletionStuckMsV2(n) {\n _llmgovStuckMs = _llmgovPos(n, \"llmgovCompletionStuckMs\");\n}\nexport function getLlmgovCompletionStuckMsV2() {\n return _llmgovStuckMs;\n}\nexport function _resetStateLlmProvidersGovV2() {\n _llmgovPsV2.clear();\n _llmgovJsV2.clear();\n _llmgovMaxActive = 8;\n _llmgovMaxPending = 25;\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000;\n _llmgovStuckMs = 60 * 1000;\n}\nexport function registerLlmgovProfileV2({\n id,\n owner,\n provider,\n metadata,\n} = {}) {\n if (!id || !owner) throw new Error(\"id and owner required\");\n if (_llmgovPsV2.has(id))\n throw new Error(`llmgov profile ${id} already exists`);\n const now = Date.now();\n const p = {\n id,\n owner,\n provider: provider || \"ollama\",\n status: LLMGOV_PROFILE_MATURITY_V2.PENDING,\n createdAt: now,\n updatedAt: now,\n lastTouchedAt: now,\n activatedAt: null,\n archivedAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovPsV2.set(id, p);\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function activateLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n const isInitial = p.status === LLMGOV_PROFILE_MATURITY_V2.PENDING;\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ACTIVE);\n if (isInitial && _llmgovCountActive(p.owner) >= _llmgovMaxActive)\n throw new Error(`max active llmgov profiles for owner ${p.owner} reached`);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ACTIVE;\n p.updatedAt = now;\n p.lastTouchedAt = now;\n if (!p.activatedAt) p.activatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function degradeLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.DEGRADED);\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = Date.now();\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function archiveLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ARCHIVED);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ARCHIVED;\n p.updatedAt = now;\n if (!p.archivedAt) p.archivedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function touchLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n if (_llmgovPTerminal.has(p.status))\n throw new Error(`cannot touch terminal llmgov profile ${id}`);\n const now = Date.now();\n p.lastTouchedAt = now;\n p.updatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function getLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) return null;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function listLlmgovProfilesV2() {\n return [..._llmgovPsV2.values()].map((p) => ({\n ...p,\n metadata: { ...p.metadata },\n }));\n}\nexport function createLlmgovCompletionV2({\n id,\n profileId,\n model,\n metadata,\n} = {}) {\n if (!id || !profileId) throw new Error(\"id and profileId required\");\n if (_llmgovJsV2.has(id))\n throw new Error(`llmgov completion ${id} already exists`);\n if (!_llmgovPsV2.has(profileId))\n throw new Error(`llmgov profile ${profileId} not found`);\n if (_llmgovCountPending(profileId) >= _llmgovMaxPending)\n throw new Error(\n `max pending llmgov completions for profile ${profileId} reached`,\n );\n const now = Date.now();\n const j = {\n id,\n profileId,\n model: model || \"\",\n status: LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n updatedAt: now,\n startedAt: null,\n settledAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovJsV2.set(id, j);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function inferringLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING;\n j.updatedAt = now;\n if (!j.startedAt) j.startedAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function completeCompletionLlmgovV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function failLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.failReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function cancelLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.cancelReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function getLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) return null;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function listLlmgovCompletionsV2() {\n return [..._llmgovJsV2.values()].map((j) => ({\n ...j,\n metadata: { ...j.metadata },\n }));\n}\nexport function autoDegradeIdleLlmgovProfilesV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const p of _llmgovPsV2.values())\n if (\n p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE &&\n t - p.lastTouchedAt >= _llmgovIdleMs\n ) {\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = t;\n flipped.push(p.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function autoFailStuckLlmgovCompletionsV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const j of _llmgovJsV2.values())\n if (\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING &&\n j.startedAt != null &&\n t - j.startedAt >= _llmgovStuckMs\n ) {\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = t;\n if (!j.settledAt) j.settledAt = t;\n j.metadata.failReason = \"auto-fail-stuck\";\n flipped.push(j.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function getLlmProvidersGovStatsV2() {\n const profilesByStatus = {};\n for (const v of Object.values(LLMGOV_PROFILE_MATURITY_V2))\n profilesByStatus[v] = 0;\n for (const p of _llmgovPsV2.values()) profilesByStatus[p.status]++;\n const completionsByStatus = {};\n for (const v of Object.values(LLMGOV_COMPLETION_LIFECYCLE_V2))\n completionsByStatus[v] = 0;\n for (const j of _llmgovJsV2.values()) completionsByStatus[j.status]++;\n return {\n totalLlmgovProfilesV2: _llmgovPsV2.size,\n totalLlmgovCompletionsV2: _llmgovJsV2.size,\n maxActiveLlmgovProfilesPerOwner: _llmgovMaxActive,\n maxPendingLlmgovCompletionsPerProfile: _llmgovMaxPending,\n llmgovProfileIdleMs: _llmgovIdleMs,\n llmgovCompletionStuckMs: _llmgovStuckMs,\n profilesByStatus,\n completionsByStatus,\n };\n}\n"}, "e2612b2f15916b59ffdb3c21a308f1a0e97bb940": {"t": 1785754080.695706, "n": 33221, "h": "e54c9970ff92948c", "c": "/**\n * LLM Provider registry — supports multiple AI providers with a unified interface.\n */\n\n/**\n * Built-in provider definitions.\n */\nexport const BUILT_IN_PROVIDERS = {\n ollama: {\n name: \"ollama\",\n displayName: \"Ollama (Local)\",\n baseUrl: \"http://localhost:11434\",\n apiKeyEnv: null,\n models: [\n \"qwen2.5:7b\",\n \"qwen2.5:14b\",\n \"qwen2.5-coder:14b\",\n \"qwen2:7b\",\n \"llama3:8b\",\n \"mistral:7b\",\n \"codellama:7b\",\n ],\n free: true,\n },\n openai: {\n name: \"openai\",\n displayName: \"OpenAI\",\n baseUrl: \"https://api.openai.com/v1\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-3.5-turbo\", \"o1\"],\n free: false,\n },\n anthropic: {\n name: \"anthropic\",\n displayName: \"Anthropic\",\n baseUrl: \"https://api.anthropic.com/v1\",\n apiKeyEnv: \"ANTHROPIC_API_KEY\",\n models: [\n \"claude-opus-4-6\",\n \"claude-sonnet-4-6\",\n \"claude-haiku-4-5-20251001\",\n ],\n free: false,\n },\n deepseek: {\n name: \"deepseek\",\n displayName: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com/v1\",\n apiKeyEnv: \"DEEPSEEK_API_KEY\",\n models: [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"],\n free: false,\n },\n dashscope: {\n name: \"dashscope\",\n displayName: \"DashScope (Alibaba)\",\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n apiKeyEnv: \"DASHSCOPE_API_KEY\",\n models: [\"qwen-turbo\", \"qwen-plus\", \"qwen-max\"],\n free: false,\n },\n gemini: {\n name: \"gemini\",\n displayName: \"Google Gemini\",\n baseUrl: \"https://generativelanguage.googleapis.com/v1beta\",\n apiKeyEnv: \"GEMINI_API_KEY\",\n models: [\"gemini-2.0-flash\", \"gemini-2.0-pro\", \"gemini-1.5-flash\"],\n free: false,\n },\n mistral: {\n name: \"mistral\",\n displayName: \"Mistral AI\",\n baseUrl: \"https://api.mistral.ai/v1\",\n apiKeyEnv: \"MISTRAL_API_KEY\",\n models: [\n \"mistral-large-latest\",\n \"mistral-medium-latest\",\n \"mistral-small-latest\",\n ],\n free: false,\n },\n volcengine: {\n name: \"volcengine\",\n displayName: \"Volcengine (火山引擎/豆包)\",\n baseUrl: \"https://ark.cn-beijing.volces.com/api/v3\",\n apiKeyEnv: \"VOLCENGINE_API_KEY\",\n models: [\n \"doubao-seed-1-6-251015\",\n \"doubao-seed-1-6-flash-250828\",\n \"doubao-seed-1-6-lite-251015\",\n \"doubao-seed-code\",\n ],\n free: false,\n },\n kimi: {\n name: \"kimi\",\n displayName: \"Kimi (月之暗面)\",\n baseUrl: \"https://api.moonshot.cn/v1\",\n apiKeyEnv: \"MOONSHOT_API_KEY\",\n models: [\n \"moonshot-v1-auto\",\n \"moonshot-v1-8k\",\n \"moonshot-v1-32k\",\n \"moonshot-v1-128k\",\n ],\n free: false,\n },\n minimax: {\n name: \"minimax\",\n displayName: \"MiniMax (海螺AI)\",\n baseUrl: \"https://api.minimax.chat/v1\",\n apiKeyEnv: \"MINIMAX_API_KEY\",\n models: [\"MiniMax-Text-01\", \"abab6.5s-chat\", \"abab5.5-chat\"],\n free: false,\n },\n};\n\n/**\n * Provider registry — manages available providers and active selection.\n */\nexport class LLMProviderRegistry {\n constructor(db) {\n this.db = db;\n this.providers = new Map();\n this._ensureTable();\n this._loadBuiltins();\n this._loadCustom();\n }\n\n _ensureTable() {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS llm_providers (\n name TEXT PRIMARY KEY,\n display_name TEXT NOT NULL,\n base_url TEXT NOT NULL,\n api_key_env TEXT,\n models TEXT DEFAULT '[]',\n is_active INTEGER DEFAULT 0,\n custom INTEGER DEFAULT 0,\n created_at TEXT DEFAULT (datetime('now'))\n )\n `);\n }\n\n _loadBuiltins() {\n for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {\n this.providers.set(name, { ...def, custom: false });\n }\n }\n\n _loadCustom() {\n const rows = this.db\n .prepare(\"SELECT * FROM llm_providers WHERE custom = 1\")\n .all();\n for (const row of rows) {\n this.providers.set(row.name, {\n name: row.name,\n displayName: row.display_name,\n baseUrl: row.base_url,\n apiKeyEnv: row.api_key_env,\n models: JSON.parse(row.models || \"[]\"),\n custom: true,\n free: false,\n });\n }\n }\n\n /**\n * List all providers.\n */\n list() {\n const result = [];\n for (const [name, provider] of this.providers) {\n const hasKey = provider.apiKeyEnv\n ? !!process.env[provider.apiKeyEnv]\n : true;\n result.push({\n name,\n displayName: provider.displayName,\n baseUrl: provider.baseUrl,\n models: provider.models,\n hasApiKey: hasKey,\n custom: provider.custom || false,\n free: provider.free || false,\n });\n }\n return result;\n }\n\n /**\n * Get a specific provider.\n */\n get(name) {\n return this.providers.get(name) || null;\n }\n\n /**\n * Add a custom provider.\n */\n addProvider(name, config) {\n const provider = {\n name,\n displayName: config.displayName || name,\n baseUrl: config.baseUrl,\n apiKeyEnv: config.apiKeyEnv || null,\n models: config.models || [],\n custom: true,\n free: config.free || false,\n };\n\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, custom) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n provider.displayName,\n provider.baseUrl,\n provider.apiKeyEnv,\n JSON.stringify(provider.models),\n );\n\n this.providers.set(name, provider);\n return provider;\n }\n\n /**\n * Remove a custom provider.\n */\n removeProvider(name) {\n const provider = this.providers.get(name);\n if (!provider) return false;\n if (!provider.custom)\n throw new Error(`Cannot remove built-in provider \"${name}\"`);\n\n this.db\n .prepare(\"DELETE FROM llm_providers WHERE name = ? AND custom = 1\")\n .run(name);\n this.providers.delete(name);\n return true;\n }\n\n /**\n * Get/set the active provider.\n */\n getActive() {\n const row = this.db\n .prepare(\"SELECT name FROM llm_providers WHERE is_active = 1\")\n .get();\n return row ? row.name : \"ollama\";\n }\n\n setActive(name) {\n if (!this.providers.has(name)) {\n throw new Error(`Provider \"${name}\" not found`);\n }\n // Reset all\n this.db.prepare(\"UPDATE llm_providers SET is_active = 0 WHERE 1=1\").run();\n // Set active\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO llm_providers (name, display_name, base_url, api_key_env, models, is_active) VALUES (?, ?, ?, ?, ?, 1)\",\n )\n .run(\n name,\n this.providers.get(name).displayName,\n this.providers.get(name).baseUrl,\n this.providers.get(name).apiKeyEnv,\n JSON.stringify(this.providers.get(name).models),\n );\n return this.providers.get(name);\n }\n\n /**\n * Get API key for a provider (from env or config).\n */\n getApiKey(name) {\n const provider = this.providers.get(name);\n if (!provider) return null;\n if (!provider.apiKeyEnv) return null;\n return process.env[provider.apiKeyEnv] || null;\n }\n\n /**\n * Test provider connectivity.\n */\n async testProvider(name, model) {\n const provider = this.providers.get(name);\n if (!provider) throw new Error(`Provider \"${name}\" not found`);\n\n const start = Date.now();\n const testModel = model || provider.models[0];\n\n if (name === \"ollama\") {\n const res = await fetch(`${provider.baseUrl}/api/generate`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: testModel, prompt: \"Hi\", stream: false }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n return {\n ok: true,\n elapsed: Date.now() - start,\n response: data.response?.trim(),\n };\n }\n\n if (name === \"gemini\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"GEMINI_API_KEY not set\");\n const res = await fetch(\n `${provider.baseUrl}/models/${testModel}:generateContent?key=${key}`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ contents: [{ parts: [{ text: \"Hi\" }] }] }),\n },\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n if (name === \"anthropic\") {\n const key = this.getApiKey(name);\n if (!key) throw new Error(\"ANTHROPIC_API_KEY not set\");\n const res = await fetch(`${provider.baseUrl}/messages`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": key,\n \"anthropic-version\": \"2023-06-01\",\n },\n body: JSON.stringify({\n model: testModel,\n max_tokens: 10,\n messages: [{ role: \"user\", content: \"Hi\" }],\n }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.content?.[0]?.text || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n\n // OpenAI-compatible (openai, deepseek, dashscope, mistral, volcengine, kimi, minimax)\n const key = this.getApiKey(name);\n if (!key) throw new Error(`${provider.apiKeyEnv} not set`);\n const res = await fetch(`${provider.baseUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${key}`,\n },\n body: JSON.stringify({\n model: testModel,\n messages: [{ role: \"user\", content: \"Hi\" }],\n max_tokens: 10,\n }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n return { ok: true, elapsed: Date.now() - start, response: text.trim() };\n }\n}\n\n/* ─────────────────────────────────────────────────────────────────\n * V2 Governance Layer (in-memory, independent of SQLite llm_providers)\n *\n * Provider profile maturity: pending → active → suspended → retired\n * - retired terminal\n * - suspended → active recovery (cap-exempt)\n *\n * Request lifecycle: queued → running → completed | failed | cancelled\n * - 3 terminals\n * - per-profile pending-request cap counts queued+running\n *\n * Per-owner active-profile cap on pending→active only (recovery exempt).\n * Per-profile pending-request cap enforced at createRequestV2.\n *\n * Auto-flip:\n * - autoSuspendIdleProfilesV2 active w/ lastSeenAt past idle threshold → suspended\n * - autoFailStuckRequestsV2 running w/ startedAt past stuck threshold → failed\n * ───────────────────────────────────────────────────────────────── */\n\nexport const PROVIDER_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n SUSPENDED: \"suspended\",\n RETIRED: \"retired\",\n});\n\nexport const REQUEST_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n RUNNING: \"running\",\n COMPLETED: \"completed\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\n\nconst _PROVIDER_TRANSITIONS_V2 = new Map([\n [\n PROVIDER_MATURITY_V2.PENDING,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.ACTIVE,\n new Set([PROVIDER_MATURITY_V2.SUSPENDED, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [\n PROVIDER_MATURITY_V2.SUSPENDED,\n new Set([PROVIDER_MATURITY_V2.ACTIVE, PROVIDER_MATURITY_V2.RETIRED]),\n ],\n [PROVIDER_MATURITY_V2.RETIRED, new Set()],\n]);\nconst _PROVIDER_TERMINALS_V2 = new Set([PROVIDER_MATURITY_V2.RETIRED]);\n\nconst _REQUEST_TRANSITIONS_V2 = new Map([\n [\n REQUEST_LIFECYCLE_V2.QUEUED,\n new Set([REQUEST_LIFECYCLE_V2.RUNNING, REQUEST_LIFECYCLE_V2.CANCELLED]),\n ],\n [\n REQUEST_LIFECYCLE_V2.RUNNING,\n new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [REQUEST_LIFECYCLE_V2.COMPLETED, new Set()],\n [REQUEST_LIFECYCLE_V2.FAILED, new Set()],\n [REQUEST_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _REQUEST_TERMINALS_V2 = new Set([\n REQUEST_LIFECYCLE_V2.COMPLETED,\n REQUEST_LIFECYCLE_V2.FAILED,\n REQUEST_LIFECYCLE_V2.CANCELLED,\n]);\n\nexport const PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER = 8;\nexport const PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE = 16;\nexport const PROVIDER_DEFAULT_PROFILE_IDLE_MS = 14 * 24 * 60 * 60 * 1000;\nexport const PROVIDER_DEFAULT_REQUEST_STUCK_MS = 5 * 60 * 1000;\n\nconst _stateLlmV2 = {\n profiles: new Map(),\n requests: new Map(),\n maxActiveProfilesPerOwner: PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER,\n maxPendingRequestsPerProfile:\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE,\n profileIdleMs: PROVIDER_DEFAULT_PROFILE_IDLE_MS,\n requestStuckMs: PROVIDER_DEFAULT_REQUEST_STUCK_MS,\n};\n\nfunction _posIntLlmV2(n, label) {\n const v = Math.floor(n);\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`${label} must be a positive integer`);\n }\n return v;\n}\n\nfunction _copyProfileV2(p) {\n return { ...p, metadata: { ...p.metadata } };\n}\n\nfunction _copyRequestV2(r) {\n return { ...r, metadata: { ...r.metadata } };\n}\n\nexport function getMaxActiveProfilesPerOwnerV2() {\n return _stateLlmV2.maxActiveProfilesPerOwner;\n}\n\nexport function setMaxActiveProfilesPerOwnerV2(n) {\n _stateLlmV2.maxActiveProfilesPerOwner = _posIntLlmV2(\n n,\n \"maxActiveProfilesPerOwner\",\n );\n}\n\nexport function getMaxPendingRequestsPerProfileV2() {\n return _stateLlmV2.maxPendingRequestsPerProfile;\n}\n\nexport function setMaxPendingRequestsPerProfileV2(n) {\n _stateLlmV2.maxPendingRequestsPerProfile = _posIntLlmV2(\n n,\n \"maxPendingRequestsPerProfile\",\n );\n}\n\nexport function getProfileIdleMsV2() {\n return _stateLlmV2.profileIdleMs;\n}\n\nexport function setProfileIdleMsV2(ms) {\n _stateLlmV2.profileIdleMs = _posIntLlmV2(ms, \"profileIdleMs\");\n}\n\nexport function getRequestStuckMsV2() {\n return _stateLlmV2.requestStuckMs;\n}\n\nexport function setRequestStuckMsV2(ms) {\n _stateLlmV2.requestStuckMs = _posIntLlmV2(ms, \"requestStuckMs\");\n}\n\nexport function getActiveProfileCountV2(ownerId) {\n let count = 0;\n for (const p of _stateLlmV2.profiles.values()) {\n if (p.ownerId === ownerId && p.status === PROVIDER_MATURITY_V2.ACTIVE) {\n count++;\n }\n }\n return count;\n}\n\nexport function getPendingRequestCountV2(profileId) {\n let count = 0;\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.profileId === profileId &&\n (r.status === REQUEST_LIFECYCLE_V2.QUEUED ||\n r.status === REQUEST_LIFECYCLE_V2.RUNNING)\n ) {\n count++;\n }\n }\n return count;\n}\n\nexport function registerProfileV2(\n id,\n { ownerId, provider, model, metadata } = {},\n) {\n if (!id) throw new Error(\"profile id is required\");\n if (!ownerId) throw new Error(\"ownerId is required\");\n if (!provider) throw new Error(\"provider is required\");\n if (_stateLlmV2.profiles.has(id))\n throw new Error(`profile ${id} already exists`);\n const now = Date.now();\n const profile = {\n id,\n ownerId,\n provider,\n model: model || \"default\",\n status: PROVIDER_MATURITY_V2.PENDING,\n createdAt: now,\n lastSeenAt: now,\n activatedAt: null,\n retiredAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.profiles.set(id, profile);\n return _copyProfileV2(profile);\n}\n\nexport function getProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n return p ? _copyProfileV2(p) : null;\n}\n\nexport function listProfilesV2({ ownerId, status, provider } = {}) {\n const out = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (ownerId && p.ownerId !== ownerId) continue;\n if (status && p.status !== status) continue;\n if (provider && p.provider !== provider) continue;\n out.push(_copyProfileV2(p));\n }\n return out;\n}\n\nexport function setProfileStatusV2(id, next) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n const allowed = _PROVIDER_TRANSITIONS_V2.get(p.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid profile transition: ${p.status} → ${next}`);\n }\n if (\n p.status === PROVIDER_MATURITY_V2.PENDING &&\n next === PROVIDER_MATURITY_V2.ACTIVE\n ) {\n const count = getActiveProfileCountV2(p.ownerId);\n if (count >= _stateLlmV2.maxActiveProfilesPerOwner) {\n throw new Error(\n `owner ${p.ownerId} active-profile cap reached (${count}/${_stateLlmV2.maxActiveProfilesPerOwner})`,\n );\n }\n }\n const now = Date.now();\n p.status = next;\n p.lastSeenAt = now;\n if (next === PROVIDER_MATURITY_V2.ACTIVE && !p.activatedAt)\n p.activatedAt = now;\n if (_PROVIDER_TERMINALS_V2.has(next) && !p.retiredAt) p.retiredAt = now;\n return _copyProfileV2(p);\n}\n\nexport function activateProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.ACTIVE);\n}\n\nexport function suspendProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.SUSPENDED);\n}\n\nexport function retireProfileV2(id) {\n return setProfileStatusV2(id, PROVIDER_MATURITY_V2.RETIRED);\n}\n\nexport function touchProfileV2(id) {\n const p = _stateLlmV2.profiles.get(id);\n if (!p) throw new Error(`profile ${id} not found`);\n p.lastSeenAt = Date.now();\n return _copyProfileV2(p);\n}\n\nexport function createRequestV2(id, { profileId, kind, metadata } = {}) {\n if (!id) throw new Error(\"request id is required\");\n if (!profileId) throw new Error(\"profileId is required\");\n if (_stateLlmV2.requests.has(id))\n throw new Error(`request ${id} already exists`);\n const profile = _stateLlmV2.profiles.get(profileId);\n if (!profile) throw new Error(`profile ${profileId} not found`);\n const pending = getPendingRequestCountV2(profileId);\n if (pending >= _stateLlmV2.maxPendingRequestsPerProfile) {\n throw new Error(\n `profile ${profileId} pending-request cap reached (${pending}/${_stateLlmV2.maxPendingRequestsPerProfile})`,\n );\n }\n const now = Date.now();\n const req = {\n id,\n profileId,\n kind: kind || \"completion\",\n status: REQUEST_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n lastSeenAt: now,\n startedAt: null,\n settledAt: null,\n metadata: metadata ? { ...metadata } : {},\n };\n _stateLlmV2.requests.set(id, req);\n return _copyRequestV2(req);\n}\n\nexport function getRequestV2(id) {\n const r = _stateLlmV2.requests.get(id);\n return r ? _copyRequestV2(r) : null;\n}\n\nexport function listRequestsV2({ profileId, status } = {}) {\n const out = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (profileId && r.profileId !== profileId) continue;\n if (status && r.status !== status) continue;\n out.push(_copyRequestV2(r));\n }\n return out;\n}\n\nexport function setRequestStatusV2(id, next) {\n const r = _stateLlmV2.requests.get(id);\n if (!r) throw new Error(`request ${id} not found`);\n const allowed = _REQUEST_TRANSITIONS_V2.get(r.status);\n if (!allowed || !allowed.has(next)) {\n throw new Error(`invalid request transition: ${r.status} → ${next}`);\n }\n const now = Date.now();\n r.status = next;\n r.lastSeenAt = now;\n if (next === REQUEST_LIFECYCLE_V2.RUNNING && !r.startedAt) r.startedAt = now;\n if (_REQUEST_TERMINALS_V2.has(next) && !r.settledAt) r.settledAt = now;\n return _copyRequestV2(r);\n}\n\nexport function startRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.RUNNING);\n}\n\nexport function completeRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.COMPLETED);\n}\n\nexport function failRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.FAILED);\n}\n\nexport function cancelRequestV2(id) {\n return setRequestStatusV2(id, REQUEST_LIFECYCLE_V2.CANCELLED);\n}\n\nexport function autoSuspendIdleProfilesV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const p of _stateLlmV2.profiles.values()) {\n if (\n p.status === PROVIDER_MATURITY_V2.ACTIVE &&\n now - p.lastSeenAt >= _stateLlmV2.profileIdleMs\n ) {\n p.status = PROVIDER_MATURITY_V2.SUSPENDED;\n p.lastSeenAt = now;\n flipped.push(p.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function autoFailStuckRequestsV2({ now = Date.now() } = {}) {\n const flipped = [];\n for (const r of _stateLlmV2.requests.values()) {\n if (\n r.status === REQUEST_LIFECYCLE_V2.RUNNING &&\n r.startedAt &&\n now - r.startedAt >= _stateLlmV2.requestStuckMs\n ) {\n r.status = REQUEST_LIFECYCLE_V2.FAILED;\n r.lastSeenAt = now;\n if (!r.settledAt) r.settledAt = now;\n flipped.push(r.id);\n }\n }\n return { flipped, count: flipped.length };\n}\n\nexport function getLlmProvidersStatsV2() {\n const profilesByStatus = {};\n for (const s of Object.values(PROVIDER_MATURITY_V2)) profilesByStatus[s] = 0;\n for (const p of _stateLlmV2.profiles.values()) profilesByStatus[p.status]++;\n const requestsByStatus = {};\n for (const s of Object.values(REQUEST_LIFECYCLE_V2)) requestsByStatus[s] = 0;\n for (const r of _stateLlmV2.requests.values()) requestsByStatus[r.status]++;\n return {\n totalProfilesV2: _stateLlmV2.profiles.size,\n totalRequestsV2: _stateLlmV2.requests.size,\n maxActiveProfilesPerOwner: _stateLlmV2.maxActiveProfilesPerOwner,\n maxPendingRequestsPerProfile: _stateLlmV2.maxPendingRequestsPerProfile,\n profileIdleMs: _stateLlmV2.profileIdleMs,\n requestStuckMs: _stateLlmV2.requestStuckMs,\n profilesByStatus,\n requestsByStatus,\n };\n}\n\nexport function _resetStateLlmProvidersV2() {\n _stateLlmV2.profiles.clear();\n _stateLlmV2.requests.clear();\n _stateLlmV2.maxActiveProfilesPerOwner = PROVIDER_DEFAULT_MAX_ACTIVE_PER_OWNER;\n _stateLlmV2.maxPendingRequestsPerProfile =\n PROVIDER_DEFAULT_MAX_PENDING_REQUESTS_PER_PROFILE;\n _stateLlmV2.profileIdleMs = PROVIDER_DEFAULT_PROFILE_IDLE_MS;\n _stateLlmV2.requestStuckMs = PROVIDER_DEFAULT_REQUEST_STUCK_MS;\n}\n\n// =====================================================================\n// llm-providers V2 governance overlay (iter22)\n// =====================================================================\nexport const LLMGOV_PROFILE_MATURITY_V2 = Object.freeze({\n PENDING: \"pending\",\n ACTIVE: \"active\",\n DEGRADED: \"degraded\",\n ARCHIVED: \"archived\",\n});\nexport const LLMGOV_COMPLETION_LIFECYCLE_V2 = Object.freeze({\n QUEUED: \"queued\",\n INFERRING: \"inferring\",\n INFERRED: \"inferred\",\n FAILED: \"failed\",\n CANCELLED: \"cancelled\",\n});\nconst _llmgovPTrans = new Map([\n [\n LLMGOV_PROFILE_MATURITY_V2.PENDING,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [\n LLMGOV_PROFILE_MATURITY_V2.DEGRADED,\n new Set([\n LLMGOV_PROFILE_MATURITY_V2.ACTIVE,\n LLMGOV_PROFILE_MATURITY_V2.ARCHIVED,\n ]),\n ],\n [LLMGOV_PROFILE_MATURITY_V2.ARCHIVED, new Set()],\n]);\nconst _llmgovPTerminal = new Set([LLMGOV_PROFILE_MATURITY_V2.ARCHIVED]);\nconst _llmgovJTrans = new Map([\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING,\n new Set([\n LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED,\n LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED,\n ]),\n ],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED, new Set()],\n [LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED, new Set()],\n]);\nconst _llmgovPsV2 = new Map();\nconst _llmgovJsV2 = new Map();\nlet _llmgovMaxActive = 8,\n _llmgovMaxPending = 25,\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000,\n _llmgovStuckMs = 60 * 1000;\nfunction _llmgovPos(n, label) {\n const v = Math.floor(Number(n));\n if (!Number.isFinite(v) || v <= 0)\n throw new Error(`${label} must be positive integer`);\n return v;\n}\nfunction _llmgovCheckP(from, to) {\n const a = _llmgovPTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov profile transition ${from} → ${to}`);\n}\nfunction _llmgovCheckJ(from, to) {\n const a = _llmgovJTrans.get(from);\n if (!a || !a.has(to))\n throw new Error(`invalid llmgov completion transition ${from} → ${to}`);\n}\nfunction _llmgovCountActive(owner) {\n let c = 0;\n for (const p of _llmgovPsV2.values())\n if (p.owner === owner && p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE)\n c++;\n return c;\n}\nfunction _llmgovCountPending(profileId) {\n let c = 0;\n for (const j of _llmgovJsV2.values())\n if (\n j.profileId === profileId &&\n (j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED ||\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING)\n )\n c++;\n return c;\n}\nexport function setMaxActiveLlmgovProfilesPerOwnerV2(n) {\n _llmgovMaxActive = _llmgovPos(n, \"maxActiveLlmgovProfilesPerOwner\");\n}\nexport function getMaxActiveLlmgovProfilesPerOwnerV2() {\n return _llmgovMaxActive;\n}\nexport function setMaxPendingLlmgovCompletionsPerProfileV2(n) {\n _llmgovMaxPending = _llmgovPos(n, \"maxPendingLlmgovCompletionsPerProfile\");\n}\nexport function getMaxPendingLlmgovCompletionsPerProfileV2() {\n return _llmgovMaxPending;\n}\nexport function setLlmgovProfileIdleMsV2(n) {\n _llmgovIdleMs = _llmgovPos(n, \"llmgovProfileIdleMs\");\n}\nexport function getLlmgovProfileIdleMsV2() {\n return _llmgovIdleMs;\n}\nexport function setLlmgovCompletionStuckMsV2(n) {\n _llmgovStuckMs = _llmgovPos(n, \"llmgovCompletionStuckMs\");\n}\nexport function getLlmgovCompletionStuckMsV2() {\n return _llmgovStuckMs;\n}\nexport function _resetStateLlmProvidersGovV2() {\n _llmgovPsV2.clear();\n _llmgovJsV2.clear();\n _llmgovMaxActive = 8;\n _llmgovMaxPending = 25;\n _llmgovIdleMs = 30 * 24 * 60 * 60 * 1000;\n _llmgovStuckMs = 60 * 1000;\n}\nexport function registerLlmgovProfileV2({\n id,\n owner,\n provider,\n metadata,\n} = {}) {\n if (!id || !owner) throw new Error(\"id and owner required\");\n if (_llmgovPsV2.has(id))\n throw new Error(`llmgov profile ${id} already exists`);\n const now = Date.now();\n const p = {\n id,\n owner,\n provider: provider || \"ollama\",\n status: LLMGOV_PROFILE_MATURITY_V2.PENDING,\n createdAt: now,\n updatedAt: now,\n lastTouchedAt: now,\n activatedAt: null,\n archivedAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovPsV2.set(id, p);\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function activateLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n const isInitial = p.status === LLMGOV_PROFILE_MATURITY_V2.PENDING;\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ACTIVE);\n if (isInitial && _llmgovCountActive(p.owner) >= _llmgovMaxActive)\n throw new Error(`max active llmgov profiles for owner ${p.owner} reached`);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ACTIVE;\n p.updatedAt = now;\n p.lastTouchedAt = now;\n if (!p.activatedAt) p.activatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function degradeLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.DEGRADED);\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = Date.now();\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function archiveLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n _llmgovCheckP(p.status, LLMGOV_PROFILE_MATURITY_V2.ARCHIVED);\n const now = Date.now();\n p.status = LLMGOV_PROFILE_MATURITY_V2.ARCHIVED;\n p.updatedAt = now;\n if (!p.archivedAt) p.archivedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function touchLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) throw new Error(`llmgov profile ${id} not found`);\n if (_llmgovPTerminal.has(p.status))\n throw new Error(`cannot touch terminal llmgov profile ${id}`);\n const now = Date.now();\n p.lastTouchedAt = now;\n p.updatedAt = now;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function getLlmgovProfileV2(id) {\n const p = _llmgovPsV2.get(id);\n if (!p) return null;\n return { ...p, metadata: { ...p.metadata } };\n}\nexport function listLlmgovProfilesV2() {\n return [..._llmgovPsV2.values()].map((p) => ({\n ...p,\n metadata: { ...p.metadata },\n }));\n}\nexport function createLlmgovCompletionV2({\n id,\n profileId,\n model,\n metadata,\n} = {}) {\n if (!id || !profileId) throw new Error(\"id and profileId required\");\n if (_llmgovJsV2.has(id))\n throw new Error(`llmgov completion ${id} already exists`);\n if (!_llmgovPsV2.has(profileId))\n throw new Error(`llmgov profile ${profileId} not found`);\n if (_llmgovCountPending(profileId) >= _llmgovMaxPending)\n throw new Error(\n `max pending llmgov completions for profile ${profileId} reached`,\n );\n const now = Date.now();\n const j = {\n id,\n profileId,\n model: model || \"\",\n status: LLMGOV_COMPLETION_LIFECYCLE_V2.QUEUED,\n createdAt: now,\n updatedAt: now,\n startedAt: null,\n settledAt: null,\n metadata: { ...(metadata || {}) },\n };\n _llmgovJsV2.set(id, j);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function inferringLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING;\n j.updatedAt = now;\n if (!j.startedAt) j.startedAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function completeCompletionLlmgovV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function failLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.failReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function cancelLlmgovCompletionV2(id, reason) {\n const j = _llmgovJsV2.get(id);\n if (!j) throw new Error(`llmgov completion ${id} not found`);\n _llmgovCheckJ(j.status, LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED);\n const now = Date.now();\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.CANCELLED;\n j.updatedAt = now;\n if (!j.settledAt) j.settledAt = now;\n if (reason) j.metadata.cancelReason = String(reason);\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function getLlmgovCompletionV2(id) {\n const j = _llmgovJsV2.get(id);\n if (!j) return null;\n return { ...j, metadata: { ...j.metadata } };\n}\nexport function listLlmgovCompletionsV2() {\n return [..._llmgovJsV2.values()].map((j) => ({\n ...j,\n metadata: { ...j.metadata },\n }));\n}\nexport function autoDegradeIdleLlmgovProfilesV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const p of _llmgovPsV2.values())\n if (\n p.status === LLMGOV_PROFILE_MATURITY_V2.ACTIVE &&\n t - p.lastTouchedAt >= _llmgovIdleMs\n ) {\n p.status = LLMGOV_PROFILE_MATURITY_V2.DEGRADED;\n p.updatedAt = t;\n flipped.push(p.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function autoFailStuckLlmgovCompletionsV2({ now } = {}) {\n const t = now ?? Date.now();\n const flipped = [];\n for (const j of _llmgovJsV2.values())\n if (\n j.status === LLMGOV_COMPLETION_LIFECYCLE_V2.INFERRING &&\n j.startedAt != null &&\n t - j.startedAt >= _llmgovStuckMs\n ) {\n j.status = LLMGOV_COMPLETION_LIFECYCLE_V2.FAILED;\n j.updatedAt = t;\n if (!j.settledAt) j.settledAt = t;\n j.metadata.failReason = \"auto-fail-stuck\";\n flipped.push(j.id);\n }\n return { flipped, count: flipped.length };\n}\nexport function getLlmProvidersGovStatsV2() {\n const profilesByStatus = {};\n for (const v of Object.values(LLMGOV_PROFILE_MATURITY_V2))\n profilesByStatus[v] = 0;\n for (const p of _llmgovPsV2.values()) profilesByStatus[p.status]++;\n const completionsByStatus = {};\n for (const v of Object.values(LLMGOV_COMPLETION_LIFECYCLE_V2))\n completionsByStatus[v] = 0;\n for (const j of _llmgovJsV2.values()) completionsByStatus[j.status]++;\n return {\n totalLlmgovProfilesV2: _llmgovPsV2.size,\n totalLlmgovCompletionsV2: _llmgovJsV2.size,\n maxActiveLlmgovProfilesPerOwner: _llmgovMaxActive,\n maxPendingLlmgovCompletionsPerProfile: _llmgovMaxPending,\n llmgovProfileIdleMs: _llmgovIdleMs,\n llmgovCompletionStuckMs: _llmgovStuckMs,\n profilesByStatus,\n completionsByStatus,\n };\n}\n"}}, "docker-compose.yml": {"9e2a3238426499a3de1d228034e66dab91cbfa2c": {"t": 1785746985.006092, "n": 10982, "h": "6e6830fa3a61fa19", "c": "version: '3.8'\n\nservices:\n # Whisper.cpp Local Speech Recognition Service (v0.27.0)\n # 高性能本地语音识别,支持 CPU 和 GPU (CUDA)\n whisper-service:\n build:\n context: ./desktop-app-vue/scripts/whisper-server\n dockerfile: Dockerfile\n args:\n - GPU_SUPPORT=${WHISPER_GPU:-cpu}\n image: chainlesschain/whisper-server:${WHISPER_GPU:-cpu}\n container_name: chainlesschain-whisper\n ports:\n - \"8002:8002\"\n environment:\n - WHISPER_MODEL_DIR=/app/models\n - WHISPER_MODEL_SIZE=${WHISPER_MODEL:-base}\n - WHISPER_SERVER_PORT=8002\n - WHISPER_DEVICE=${WHISPER_DEVICE:-auto}\n - WHISPER_MAX_FILE_SIZE=52428800\n volumes:\n - whisper-models:/app/models\n restart: unless-stopped\n healthcheck:\n test: [\"CMD-SHELL\", \"curl -f http://localhost:8002/health || exit 1\"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 60s\n networks:\n - chainlesschain-network\n # GPU 支持(需要 nvidia-container-toolkit)\n # deploy:\n # resources:\n # reservations:\n # devices:\n # - driver: nvidia\n # count: 1\n # capabilities: [gpu]\n\n # Ollama - 本地LLM服务(CPU版本)\n ollama:\n image: ollama/ollama:latest\n container_name: chainlesschain-ollama\n volumes:\n - ./data/ollama:/root/.ollama\n ports:\n - \"11434:11434\"\n environment:\n - OLLAMA_MODELS=/root/.ollama/models\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Qdrant - 向量数据库(后端AI服务使用)\n qdrant:\n image: qdrant/qdrant:latest\n container_name: chainlesschain-qdrant\n volumes:\n - ./data/qdrant:/qdrant/storage\n ports:\n - \"6333:6333\"\n - \"6334:6334\"\n environment:\n - QDRANT__SERVICE__GRPC_PORT=6334\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # ChromaDB - 向量数据库(Desktop App使用)\n chromadb:\n image: chromadb/chroma:latest\n container_name: chainlesschain-chromadb\n volumes:\n - ./data/chromadb:/chroma/chroma\n ports:\n - \"8000:8000\"\n environment:\n - IS_PERSISTENT=TRUE\n - ANONYMIZED_TELEMETRY=False\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # PostgreSQL - 项目元数据数据库\n postgres:\n image: postgres:16-alpine\n container_name: chainlesschain-postgres\n volumes:\n - ./data/postgres:/var/lib/postgresql/data\n # Note: Flyway handles migrations, don't use docker-entrypoint-initdb.d\n ports:\n - \"5432:5432\"\n environment:\n - POSTGRES_DB=chainlesschain\n - POSTGRES_USER=chainlesschain\n - POSTGRES_PASSWORD=chainlesschain_pwd_2024\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U chainlesschain -d chainlesschain\"]\n interval: 10s\n timeout: 5s\n retries: 5\n start_period: 10s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Redis - 缓存和消息队列\n redis:\n image: redis:7-alpine\n container_name: chainlesschain-redis\n volumes:\n - ./data/redis:/data\n ports:\n - \"6379:6379\"\n command: redis-server --appendonly yes --requirepass chainlesschain_redis_2024\n healthcheck:\n test: [\"CMD\", \"redis-cli\", \"--raw\", \"incr\", \"ping\"]\n interval: 10s\n timeout: 3s\n retries: 5\n start_period: 5s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # AI服务 - FastAPI Python服务\n ai-service:\n build:\n context: ./backend/ai-service\n dockerfile: Dockerfile\n container_name: chainlesschain-ai-service\n volumes:\n - ./backend/ai-service:/app\n - ./data/projects:/data/projects\n ports:\n - \"8001:8000\"\n env_file:\n - ./.env # 使用项目根目录的 .env 文件\n environment:\n # 基础服务连接\n - OLLAMA_HOST=http://ollama:11434\n - QDRANT_HOST=qdrant\n - QDRANT_PORT=6333\n - REDIS_HOST=redis\n - REDIS_PASSWORD=chainlesschain_redis_2024\n # HuggingFace镜像配置(解决国内网络访问问题)\n - HF_ENDPOINT=https://hf-mirror.com\n - TRANSFORMERS_OFFLINE=0\n # LLM配置 - 默认使用云LLM(dashscope),可在.env中配置API Key\n - LLM_PROVIDER=${LLM_PROVIDER:-dashscope}\n - LLM_MODEL=${LLM_MODEL:-qwen-turbo}\n # 云LLM API Keys(从.env文件读取)\n - DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY:-}\n - OPENAI_API_KEY=${OPENAI_API_KEY:-}\n - ZHIPU_API_KEY=${ZHIPU_API_KEY:-}\n - VOLCENGINE_API_KEY=${VOLCENGINE_API_KEY:-}\n - QIANFAN_API_KEY=${QIANFAN_API_KEY:-}\n - HUNYUAN_API_KEY=${HUNYUAN_API_KEY:-}\n - SPARK_API_KEY=${SPARK_API_KEY:-}\n - MINIMAX_API_KEY=${MINIMAX_API_KEY:-}\n - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY:-}\n depends_on:\n ollama:\n condition: service_started\n qdrant:\n condition: service_started\n redis:\n condition: service_healthy\n healthcheck:\n test: [\"CMD-SHELL\", \"python -c 'import urllib.request; urllib.request.urlopen(\\\"http://localhost:8000/health\\\")' || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 5\n start_period: 30s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 项目服务 - Spring Boot服务\n project-service:\n build:\n context: ./backend/project-service\n dockerfile: Dockerfile\n container_name: chainlesschain-project-service\n volumes:\n - ./data/projects:/data/projects\n ports:\n - \"9090:9090\"\n environment:\n # Spring Boot标准环境变量\n - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/chainlesschain\n - SPRING_DATASOURCE_USERNAME=chainlesschain\n - SPRING_DATASOURCE_PASSWORD=chainlesschain_pwd_2024\n - SPRING_DATA_REDIS_HOST=redis\n - SPRING_DATA_REDIS_PORT=6379\n - SPRING_DATA_REDIS_PASSWORD=chainlesschain_redis_2024\n # 自定义环境变量(application.yml中使用)\n - DB_HOST=postgres\n - DB_PORT=5432\n - DB_NAME=chainlesschain\n - DB_USER=chainlesschain\n - DB_PASSWORD=chainlesschain_pwd_2024\n - REDIS_HOST=redis\n - REDIS_PORT=6379\n - REDIS_PASSWORD=chainlesschain_redis_2024\n - AI_SERVICE_URL=http://ai-service:8000\n depends_on:\n postgres:\n condition: service_healthy\n redis:\n condition: service_healthy\n ai-service:\n condition: service_started\n healthcheck:\n test: [\"CMD-SHELL\", \"wget --no-verbose --tries=1 --spider http://localhost:9090/actuator/health || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 5\n start_period: 60s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 信令服务器 - WebSocket信令服务(移动端与PC端P2P通讯)\n signaling-server:\n build:\n context: ./signaling-server\n dockerfile: Dockerfile\n container_name: chainlesschain-signaling-server\n ports:\n - \"9001:9001\"\n - \"9002:9002\"\n environment:\n - PORT=9001\n - HEALTH_PORT=9002\n healthcheck:\n test: [\"CMD-SHELL\", \"wget --no-verbose --tries=1 --spider http://localhost:9002/health || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 3\n start_period: 10s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # MinIO - 插件市场对象存储(plugin .zip 包)\n marketplace-minio:\n image: minio/minio:latest\n container_name: chainlesschain-marketplace-minio\n command: server /data --console-address \":9011\"\n environment:\n - MINIO_ROOT_USER=${MARKETPLACE_MINIO_ACCESS_KEY:-minioadmin}\n - MINIO_ROOT_PASSWORD=${MARKETPLACE_MINIO_SECRET_KEY:-minioadmin123}\n ports:\n - \"9000:9000\" # S3 API\n - \"9011:9011\" # Console (9001 已被 signaling-server 占用)\n volumes:\n - ./data/marketplace-minio:/data\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:9000/minio/health/live\"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 20s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 插件市场数据库初始化(一次性,幂等)\n marketplace-db-init:\n image: postgres:16-alpine\n container_name: chainlesschain-marketplace-db-init\n depends_on:\n postgres:\n condition: service_healthy\n environment:\n - PGHOST=postgres\n - PGUSER=chainlesschain\n - PGPASSWORD=chainlesschain_pwd_2024\n - DB_NAME=plugin_marketplace\n - SCHEMA_FILE=/schema.sql\n volumes:\n - ./backend/plugin-marketplace-service/src/main/resources/schema.sql:/schema.sql:ro\n - ./deploy/init-marketplace-db.sh:/init.sh:ro\n entrypoint: [\"sh\", \"/init.sh\"]\n networks:\n - chainlesschain-network\n restart: \"no\"\n\n # 插件市场服务 - Spring Boot\n plugin-marketplace-service:\n build:\n context: ./backend/plugin-marketplace-service\n dockerfile: Dockerfile\n image: chainlesschain/plugin-marketplace-service:1.0.0\n container_name: chainlesschain-plugin-marketplace\n ports:\n - \"8090:8090\"\n environment:\n - SPRING_PROFILES_ACTIVE=prod\n # 复用根 postgres,使用独立库 plugin_marketplace\n - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/plugin_marketplace\n - SPRING_DATASOURCE_USERNAME=chainlesschain\n - SPRING_DATASOURCE_PASSWORD=chainlesschain_pwd_2024\n # 复用根 redis,用 database 2 避免与 project-service(0) 冲突\n - SPRING_DATA_REDIS_HOST=redis\n - SPRING_DATA_REDIS_PORT=6379\n - SPRING_DATA_REDIS_PASSWORD=chainlesschain_redis_2024\n - SPRING_DATA_REDIS_DATABASE=2\n # MinIO(专属,不复用其它服务)\n - MINIO_ENDPOINT=http://marketplace-minio:9000\n - MINIO_ACCESS_KEY=${MARKETPLACE_MINIO_ACCESS_KEY:-minioadmin}\n - MINIO_SECRET_KEY=${MARKETPLACE_MINIO_SECRET_KEY:-minioadmin123}\n - MINIO_BUCKET=plugins\n # JWT 必须在 .env 覆盖,默认值仅用于本地启动\n - JWT_SECRET=${PLUGIN_MARKETPLACE_JWT_SECRET:-CHANGE_ME_IN_PROD_at_least_256_bits_required_xxxxxxxxxxxxxxxxxx}\n - JWT_EXPIRATION=86400000\n - SERVER_PORT=8090\n - JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseG1GC\n depends_on:\n postgres:\n condition: service_healthy\n redis:\n condition: service_healthy\n marketplace-minio:\n condition: service_healthy\n marketplace-db-init:\n condition: service_completed_successfully\n healthcheck:\n test: [\"CMD-SHELL\", \"curl -f http://localhost:8090/api/actuator/health || exit 1\"]\n interval: 30s\n timeout: 10s\n retries: 5\n start_period: 90s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Coturn - STUN/TURN服务器(P2P NAT穿透)\n coturn:\n build:\n context: ./backend/coturn-service\n dockerfile: Dockerfile\n container_name: chainlesschain-coturn\n ports:\n - \"3478:3478\"\n - \"3478:3478/udp\"\n - \"5349:5349\"\n - \"5349:5349/udp\"\n - \"49152-49252:49152-49252/udp\" # TURN relay端口范围(限制为100个端口)\n volumes:\n - ./backend/coturn-service/turnserver.conf:/etc/coturn/turnserver.conf\n - coturn-logs:/var/log/coturn\n environment:\n - DETECT_EXTERNAL_IP=yes\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\nnetworks:\n chainlesschain-network:\n driver: bridge\n\nvolumes:\n ollama-data:\n qdrant-data:\n postgres-data:\n redis-data:\n whisper-models:\n coturn-logs:\n marketplace-minio-data:\n"}, "19dcdea87a87892fe9eb22a23b4f3fe9ce05af93": {"t": 1785747389.079283, "n": 10982, "h": "6e6830fa3a61fa19", "c": "version: '3.8'\n\nservices:\n # Whisper.cpp Local Speech Recognition Service (v0.27.0)\n # 高性能本地语音识别,支持 CPU 和 GPU (CUDA)\n whisper-service:\n build:\n context: ./desktop-app-vue/scripts/whisper-server\n dockerfile: Dockerfile\n args:\n - GPU_SUPPORT=${WHISPER_GPU:-cpu}\n image: chainlesschain/whisper-server:${WHISPER_GPU:-cpu}\n container_name: chainlesschain-whisper\n ports:\n - \"8002:8002\"\n environment:\n - WHISPER_MODEL_DIR=/app/models\n - WHISPER_MODEL_SIZE=${WHISPER_MODEL:-base}\n - WHISPER_SERVER_PORT=8002\n - WHISPER_DEVICE=${WHISPER_DEVICE:-auto}\n - WHISPER_MAX_FILE_SIZE=52428800\n volumes:\n - whisper-models:/app/models\n restart: unless-stopped\n healthcheck:\n test: [\"CMD-SHELL\", \"curl -f http://localhost:8002/health || exit 1\"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 60s\n networks:\n - chainlesschain-network\n # GPU 支持(需要 nvidia-container-toolkit)\n # deploy:\n # resources:\n # reservations:\n # devices:\n # - driver: nvidia\n # count: 1\n # capabilities: [gpu]\n\n # Ollama - 本地LLM服务(CPU版本)\n ollama:\n image: ollama/ollama:latest\n container_name: chainlesschain-ollama\n volumes:\n - ./data/ollama:/root/.ollama\n ports:\n - \"11434:11434\"\n environment:\n - OLLAMA_MODELS=/root/.ollama/models\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Qdrant - 向量数据库(后端AI服务使用)\n qdrant:\n image: qdrant/qdrant:latest\n container_name: chainlesschain-qdrant\n volumes:\n - ./data/qdrant:/qdrant/storage\n ports:\n - \"6333:6333\"\n - \"6334:6334\"\n environment:\n - QDRANT__SERVICE__GRPC_PORT=6334\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # ChromaDB - 向量数据库(Desktop App使用)\n chromadb:\n image: chromadb/chroma:latest\n container_name: chainlesschain-chromadb\n volumes:\n - ./data/chromadb:/chroma/chroma\n ports:\n - \"8000:8000\"\n environment:\n - IS_PERSISTENT=TRUE\n - ANONYMIZED_TELEMETRY=False\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # PostgreSQL - 项目元数据数据库\n postgres:\n image: postgres:16-alpine\n container_name: chainlesschain-postgres\n volumes:\n - ./data/postgres:/var/lib/postgresql/data\n # Note: Flyway handles migrations, don't use docker-entrypoint-initdb.d\n ports:\n - \"5432:5432\"\n environment:\n - POSTGRES_DB=chainlesschain\n - POSTGRES_USER=chainlesschain\n - POSTGRES_PASSWORD=chainlesschain_pwd_2024\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U chainlesschain -d chainlesschain\"]\n interval: 10s\n timeout: 5s\n retries: 5\n start_period: 10s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Redis - 缓存和消息队列\n redis:\n image: redis:7-alpine\n container_name: chainlesschain-redis\n volumes:\n - ./data/redis:/data\n ports:\n - \"6379:6379\"\n command: redis-server --appendonly yes --requirepass chainlesschain_redis_2024\n healthcheck:\n test: [\"CMD\", \"redis-cli\", \"--raw\", \"incr\", \"ping\"]\n interval: 10s\n timeout: 3s\n retries: 5\n start_period: 5s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # AI服务 - FastAPI Python服务\n ai-service:\n build:\n context: ./backend/ai-service\n dockerfile: Dockerfile\n container_name: chainlesschain-ai-service\n volumes:\n - ./backend/ai-service:/app\n - ./data/projects:/data/projects\n ports:\n - \"8001:8000\"\n env_file:\n - ./.env # 使用项目根目录的 .env 文件\n environment:\n # 基础服务连接\n - OLLAMA_HOST=http://ollama:11434\n - QDRANT_HOST=qdrant\n - QDRANT_PORT=6333\n - REDIS_HOST=redis\n - REDIS_PASSWORD=chainlesschain_redis_2024\n # HuggingFace镜像配置(解决国内网络访问问题)\n - HF_ENDPOINT=https://hf-mirror.com\n - TRANSFORMERS_OFFLINE=0\n # LLM配置 - 默认使用云LLM(dashscope),可在.env中配置API Key\n - LLM_PROVIDER=${LLM_PROVIDER:-dashscope}\n - LLM_MODEL=${LLM_MODEL:-qwen-turbo}\n # 云LLM API Keys(从.env文件读取)\n - DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY:-}\n - OPENAI_API_KEY=${OPENAI_API_KEY:-}\n - ZHIPU_API_KEY=${ZHIPU_API_KEY:-}\n - VOLCENGINE_API_KEY=${VOLCENGINE_API_KEY:-}\n - QIANFAN_API_KEY=${QIANFAN_API_KEY:-}\n - HUNYUAN_API_KEY=${HUNYUAN_API_KEY:-}\n - SPARK_API_KEY=${SPARK_API_KEY:-}\n - MINIMAX_API_KEY=${MINIMAX_API_KEY:-}\n - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY:-}\n depends_on:\n ollama:\n condition: service_started\n qdrant:\n condition: service_started\n redis:\n condition: service_healthy\n healthcheck:\n test: [\"CMD-SHELL\", \"python -c 'import urllib.request; urllib.request.urlopen(\\\"http://localhost:8000/health\\\")' || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 5\n start_period: 30s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 项目服务 - Spring Boot服务\n project-service:\n build:\n context: ./backend/project-service\n dockerfile: Dockerfile\n container_name: chainlesschain-project-service\n volumes:\n - ./data/projects:/data/projects\n ports:\n - \"9090:9090\"\n environment:\n # Spring Boot标准环境变量\n - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/chainlesschain\n - SPRING_DATASOURCE_USERNAME=chainlesschain\n - SPRING_DATASOURCE_PASSWORD=chainlesschain_pwd_2024\n - SPRING_DATA_REDIS_HOST=redis\n - SPRING_DATA_REDIS_PORT=6379\n - SPRING_DATA_REDIS_PASSWORD=chainlesschain_redis_2024\n # 自定义环境变量(application.yml中使用)\n - DB_HOST=postgres\n - DB_PORT=5432\n - DB_NAME=chainlesschain\n - DB_USER=chainlesschain\n - DB_PASSWORD=chainlesschain_pwd_2024\n - REDIS_HOST=redis\n - REDIS_PORT=6379\n - REDIS_PASSWORD=chainlesschain_redis_2024\n - AI_SERVICE_URL=http://ai-service:8000\n depends_on:\n postgres:\n condition: service_healthy\n redis:\n condition: service_healthy\n ai-service:\n condition: service_started\n healthcheck:\n test: [\"CMD-SHELL\", \"wget --no-verbose --tries=1 --spider http://localhost:9090/actuator/health || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 5\n start_period: 60s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 信令服务器 - WebSocket信令服务(移动端与PC端P2P通讯)\n signaling-server:\n build:\n context: ./signaling-server\n dockerfile: Dockerfile\n container_name: chainlesschain-signaling-server\n ports:\n - \"9001:9001\"\n - \"9002:9002\"\n environment:\n - PORT=9001\n - HEALTH_PORT=9002\n healthcheck:\n test: [\"CMD-SHELL\", \"wget --no-verbose --tries=1 --spider http://localhost:9002/health || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 3\n start_period: 10s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # MinIO - 插件市场对象存储(plugin .zip 包)\n marketplace-minio:\n image: minio/minio:latest\n container_name: chainlesschain-marketplace-minio\n command: server /data --console-address \":9011\"\n environment:\n - MINIO_ROOT_USER=${MARKETPLACE_MINIO_ACCESS_KEY:-minioadmin}\n - MINIO_ROOT_PASSWORD=${MARKETPLACE_MINIO_SECRET_KEY:-minioadmin123}\n ports:\n - \"9000:9000\" # S3 API\n - \"9011:9011\" # Console (9001 已被 signaling-server 占用)\n volumes:\n - ./data/marketplace-minio:/data\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:9000/minio/health/live\"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 20s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 插件市场数据库初始化(一次性,幂等)\n marketplace-db-init:\n image: postgres:16-alpine\n container_name: chainlesschain-marketplace-db-init\n depends_on:\n postgres:\n condition: service_healthy\n environment:\n - PGHOST=postgres\n - PGUSER=chainlesschain\n - PGPASSWORD=chainlesschain_pwd_2024\n - DB_NAME=plugin_marketplace\n - SCHEMA_FILE=/schema.sql\n volumes:\n - ./backend/plugin-marketplace-service/src/main/resources/schema.sql:/schema.sql:ro\n - ./deploy/init-marketplace-db.sh:/init.sh:ro\n entrypoint: [\"sh\", \"/init.sh\"]\n networks:\n - chainlesschain-network\n restart: \"no\"\n\n # 插件市场服务 - Spring Boot\n plugin-marketplace-service:\n build:\n context: ./backend/plugin-marketplace-service\n dockerfile: Dockerfile\n image: chainlesschain/plugin-marketplace-service:1.0.0\n container_name: chainlesschain-plugin-marketplace\n ports:\n - \"8090:8090\"\n environment:\n - SPRING_PROFILES_ACTIVE=prod\n # 复用根 postgres,使用独立库 plugin_marketplace\n - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/plugin_marketplace\n - SPRING_DATASOURCE_USERNAME=chainlesschain\n - SPRING_DATASOURCE_PASSWORD=chainlesschain_pwd_2024\n # 复用根 redis,用 database 2 避免与 project-service(0) 冲突\n - SPRING_DATA_REDIS_HOST=redis\n - SPRING_DATA_REDIS_PORT=6379\n - SPRING_DATA_REDIS_PASSWORD=chainlesschain_redis_2024\n - SPRING_DATA_REDIS_DATABASE=2\n # MinIO(专属,不复用其它服务)\n - MINIO_ENDPOINT=http://marketplace-minio:9000\n - MINIO_ACCESS_KEY=${MARKETPLACE_MINIO_ACCESS_KEY:-minioadmin}\n - MINIO_SECRET_KEY=${MARKETPLACE_MINIO_SECRET_KEY:-minioadmin123}\n - MINIO_BUCKET=plugins\n # JWT 必须在 .env 覆盖,默认值仅用于本地启动\n - JWT_SECRET=${PLUGIN_MARKETPLACE_JWT_SECRET:-CHANGE_ME_IN_PROD_at_least_256_bits_required_xxxxxxxxxxxxxxxxxx}\n - JWT_EXPIRATION=86400000\n - SERVER_PORT=8090\n - JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseG1GC\n depends_on:\n postgres:\n condition: service_healthy\n redis:\n condition: service_healthy\n marketplace-minio:\n condition: service_healthy\n marketplace-db-init:\n condition: service_completed_successfully\n healthcheck:\n test: [\"CMD-SHELL\", \"curl -f http://localhost:8090/api/actuator/health || exit 1\"]\n interval: 30s\n timeout: 10s\n retries: 5\n start_period: 90s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Coturn - STUN/TURN服务器(P2P NAT穿透)\n coturn:\n build:\n context: ./backend/coturn-service\n dockerfile: Dockerfile\n container_name: chainlesschain-coturn\n ports:\n - \"3478:3478\"\n - \"3478:3478/udp\"\n - \"5349:5349\"\n - \"5349:5349/udp\"\n - \"49152-49252:49152-49252/udp\" # TURN relay端口范围(限制为100个端口)\n volumes:\n - ./backend/coturn-service/turnserver.conf:/etc/coturn/turnserver.conf\n - coturn-logs:/var/log/coturn\n environment:\n - DETECT_EXTERNAL_IP=yes\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\nnetworks:\n chainlesschain-network:\n driver: bridge\n\nvolumes:\n ollama-data:\n qdrant-data:\n postgres-data:\n redis-data:\n whisper-models:\n coturn-logs:\n marketplace-minio-data:\n"}, "3b0b2dc09dceaf104b65bdb7bdc4448fc55e53a5": {"t": 1785754069.1208076, "n": 10982, "h": "6e6830fa3a61fa19", "c": "version: '3.8'\n\nservices:\n # Whisper.cpp Local Speech Recognition Service (v0.27.0)\n # 高性能本地语音识别,支持 CPU 和 GPU (CUDA)\n whisper-service:\n build:\n context: ./desktop-app-vue/scripts/whisper-server\n dockerfile: Dockerfile\n args:\n - GPU_SUPPORT=${WHISPER_GPU:-cpu}\n image: chainlesschain/whisper-server:${WHISPER_GPU:-cpu}\n container_name: chainlesschain-whisper\n ports:\n - \"8002:8002\"\n environment:\n - WHISPER_MODEL_DIR=/app/models\n - WHISPER_MODEL_SIZE=${WHISPER_MODEL:-base}\n - WHISPER_SERVER_PORT=8002\n - WHISPER_DEVICE=${WHISPER_DEVICE:-auto}\n - WHISPER_MAX_FILE_SIZE=52428800\n volumes:\n - whisper-models:/app/models\n restart: unless-stopped\n healthcheck:\n test: [\"CMD-SHELL\", \"curl -f http://localhost:8002/health || exit 1\"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 60s\n networks:\n - chainlesschain-network\n # GPU 支持(需要 nvidia-container-toolkit)\n # deploy:\n # resources:\n # reservations:\n # devices:\n # - driver: nvidia\n # count: 1\n # capabilities: [gpu]\n\n # Ollama - 本地LLM服务(CPU版本)\n ollama:\n image: ollama/ollama:latest\n container_name: chainlesschain-ollama\n volumes:\n - ./data/ollama:/root/.ollama\n ports:\n - \"11434:11434\"\n environment:\n - OLLAMA_MODELS=/root/.ollama/models\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Qdrant - 向量数据库(后端AI服务使用)\n qdrant:\n image: qdrant/qdrant:latest\n container_name: chainlesschain-qdrant\n volumes:\n - ./data/qdrant:/qdrant/storage\n ports:\n - \"6333:6333\"\n - \"6334:6334\"\n environment:\n - QDRANT__SERVICE__GRPC_PORT=6334\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # ChromaDB - 向量数据库(Desktop App使用)\n chromadb:\n image: chromadb/chroma:latest\n container_name: chainlesschain-chromadb\n volumes:\n - ./data/chromadb:/chroma/chroma\n ports:\n - \"8000:8000\"\n environment:\n - IS_PERSISTENT=TRUE\n - ANONYMIZED_TELEMETRY=False\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # PostgreSQL - 项目元数据数据库\n postgres:\n image: postgres:16-alpine\n container_name: chainlesschain-postgres\n volumes:\n - ./data/postgres:/var/lib/postgresql/data\n # Note: Flyway handles migrations, don't use docker-entrypoint-initdb.d\n ports:\n - \"5432:5432\"\n environment:\n - POSTGRES_DB=chainlesschain\n - POSTGRES_USER=chainlesschain\n - POSTGRES_PASSWORD=chainlesschain_pwd_2024\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U chainlesschain -d chainlesschain\"]\n interval: 10s\n timeout: 5s\n retries: 5\n start_period: 10s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Redis - 缓存和消息队列\n redis:\n image: redis:7-alpine\n container_name: chainlesschain-redis\n volumes:\n - ./data/redis:/data\n ports:\n - \"6379:6379\"\n command: redis-server --appendonly yes --requirepass chainlesschain_redis_2024\n healthcheck:\n test: [\"CMD\", \"redis-cli\", \"--raw\", \"incr\", \"ping\"]\n interval: 10s\n timeout: 3s\n retries: 5\n start_period: 5s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # AI服务 - FastAPI Python服务\n ai-service:\n build:\n context: ./backend/ai-service\n dockerfile: Dockerfile\n container_name: chainlesschain-ai-service\n volumes:\n - ./backend/ai-service:/app\n - ./data/projects:/data/projects\n ports:\n - \"8001:8000\"\n env_file:\n - ./.env # 使用项目根目录的 .env 文件\n environment:\n # 基础服务连接\n - OLLAMA_HOST=http://ollama:11434\n - QDRANT_HOST=qdrant\n - QDRANT_PORT=6333\n - REDIS_HOST=redis\n - REDIS_PASSWORD=chainlesschain_redis_2024\n # HuggingFace镜像配置(解决国内网络访问问题)\n - HF_ENDPOINT=https://hf-mirror.com\n - TRANSFORMERS_OFFLINE=0\n # LLM配置 - 默认使用云LLM(dashscope),可在.env中配置API Key\n - LLM_PROVIDER=${LLM_PROVIDER:-dashscope}\n - LLM_MODEL=${LLM_MODEL:-qwen-turbo}\n # 云LLM API Keys(从.env文件读取)\n - DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY:-}\n - OPENAI_API_KEY=${OPENAI_API_KEY:-}\n - ZHIPU_API_KEY=${ZHIPU_API_KEY:-}\n - VOLCENGINE_API_KEY=${VOLCENGINE_API_KEY:-}\n - QIANFAN_API_KEY=${QIANFAN_API_KEY:-}\n - HUNYUAN_API_KEY=${HUNYUAN_API_KEY:-}\n - SPARK_API_KEY=${SPARK_API_KEY:-}\n - MINIMAX_API_KEY=${MINIMAX_API_KEY:-}\n - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY:-}\n depends_on:\n ollama:\n condition: service_started\n qdrant:\n condition: service_started\n redis:\n condition: service_healthy\n healthcheck:\n test: [\"CMD-SHELL\", \"python -c 'import urllib.request; urllib.request.urlopen(\\\"http://localhost:8000/health\\\")' || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 5\n start_period: 30s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 项目服务 - Spring Boot服务\n project-service:\n build:\n context: ./backend/project-service\n dockerfile: Dockerfile\n container_name: chainlesschain-project-service\n volumes:\n - ./data/projects:/data/projects\n ports:\n - \"9090:9090\"\n environment:\n # Spring Boot标准环境变量\n - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/chainlesschain\n - SPRING_DATASOURCE_USERNAME=chainlesschain\n - SPRING_DATASOURCE_PASSWORD=chainlesschain_pwd_2024\n - SPRING_DATA_REDIS_HOST=redis\n - SPRING_DATA_REDIS_PORT=6379\n - SPRING_DATA_REDIS_PASSWORD=chainlesschain_redis_2024\n # 自定义环境变量(application.yml中使用)\n - DB_HOST=postgres\n - DB_PORT=5432\n - DB_NAME=chainlesschain\n - DB_USER=chainlesschain\n - DB_PASSWORD=chainlesschain_pwd_2024\n - REDIS_HOST=redis\n - REDIS_PORT=6379\n - REDIS_PASSWORD=chainlesschain_redis_2024\n - AI_SERVICE_URL=http://ai-service:8000\n depends_on:\n postgres:\n condition: service_healthy\n redis:\n condition: service_healthy\n ai-service:\n condition: service_started\n healthcheck:\n test: [\"CMD-SHELL\", \"wget --no-verbose --tries=1 --spider http://localhost:9090/actuator/health || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 5\n start_period: 60s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 信令服务器 - WebSocket信令服务(移动端与PC端P2P通讯)\n signaling-server:\n build:\n context: ./signaling-server\n dockerfile: Dockerfile\n container_name: chainlesschain-signaling-server\n ports:\n - \"9001:9001\"\n - \"9002:9002\"\n environment:\n - PORT=9001\n - HEALTH_PORT=9002\n healthcheck:\n test: [\"CMD-SHELL\", \"wget --no-verbose --tries=1 --spider http://localhost:9002/health || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 3\n start_period: 10s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # MinIO - 插件市场对象存储(plugin .zip 包)\n marketplace-minio:\n image: minio/minio:latest\n container_name: chainlesschain-marketplace-minio\n command: server /data --console-address \":9011\"\n environment:\n - MINIO_ROOT_USER=${MARKETPLACE_MINIO_ACCESS_KEY:-minioadmin}\n - MINIO_ROOT_PASSWORD=${MARKETPLACE_MINIO_SECRET_KEY:-minioadmin123}\n ports:\n - \"9000:9000\" # S3 API\n - \"9011:9011\" # Console (9001 已被 signaling-server 占用)\n volumes:\n - ./data/marketplace-minio:/data\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:9000/minio/health/live\"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 20s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 插件市场数据库初始化(一次性,幂等)\n marketplace-db-init:\n image: postgres:16-alpine\n container_name: chainlesschain-marketplace-db-init\n depends_on:\n postgres:\n condition: service_healthy\n environment:\n - PGHOST=postgres\n - PGUSER=chainlesschain\n - PGPASSWORD=chainlesschain_pwd_2024\n - DB_NAME=plugin_marketplace\n - SCHEMA_FILE=/schema.sql\n volumes:\n - ./backend/plugin-marketplace-service/src/main/resources/schema.sql:/schema.sql:ro\n - ./deploy/init-marketplace-db.sh:/init.sh:ro\n entrypoint: [\"sh\", \"/init.sh\"]\n networks:\n - chainlesschain-network\n restart: \"no\"\n\n # 插件市场服务 - Spring Boot\n plugin-marketplace-service:\n build:\n context: ./backend/plugin-marketplace-service\n dockerfile: Dockerfile\n image: chainlesschain/plugin-marketplace-service:1.0.0\n container_name: chainlesschain-plugin-marketplace\n ports:\n - \"8090:8090\"\n environment:\n - SPRING_PROFILES_ACTIVE=prod\n # 复用根 postgres,使用独立库 plugin_marketplace\n - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/plugin_marketplace\n - SPRING_DATASOURCE_USERNAME=chainlesschain\n - SPRING_DATASOURCE_PASSWORD=chainlesschain_pwd_2024\n # 复用根 redis,用 database 2 避免与 project-service(0) 冲突\n - SPRING_DATA_REDIS_HOST=redis\n - SPRING_DATA_REDIS_PORT=6379\n - SPRING_DATA_REDIS_PASSWORD=chainlesschain_redis_2024\n - SPRING_DATA_REDIS_DATABASE=2\n # MinIO(专属,不复用其它服务)\n - MINIO_ENDPOINT=http://marketplace-minio:9000\n - MINIO_ACCESS_KEY=${MARKETPLACE_MINIO_ACCESS_KEY:-minioadmin}\n - MINIO_SECRET_KEY=${MARKETPLACE_MINIO_SECRET_KEY:-minioadmin123}\n - MINIO_BUCKET=plugins\n # JWT 必须在 .env 覆盖,默认值仅用于本地启动\n - JWT_SECRET=${PLUGIN_MARKETPLACE_JWT_SECRET:-CHANGE_ME_IN_PROD_at_least_256_bits_required_xxxxxxxxxxxxxxxxxx}\n - JWT_EXPIRATION=86400000\n - SERVER_PORT=8090\n - JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseG1GC\n depends_on:\n postgres:\n condition: service_healthy\n redis:\n condition: service_healthy\n marketplace-minio:\n condition: service_healthy\n marketplace-db-init:\n condition: service_completed_successfully\n healthcheck:\n test: [\"CMD-SHELL\", \"curl -f http://localhost:8090/api/actuator/health || exit 1\"]\n interval: 30s\n timeout: 10s\n retries: 5\n start_period: 90s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Coturn - STUN/TURN服务器(P2P NAT穿透)\n coturn:\n build:\n context: ./backend/coturn-service\n dockerfile: Dockerfile\n container_name: chainlesschain-coturn\n ports:\n - \"3478:3478\"\n - \"3478:3478/udp\"\n - \"5349:5349\"\n - \"5349:5349/udp\"\n - \"49152-49252:49152-49252/udp\" # TURN relay端口范围(限制为100个端口)\n volumes:\n - ./backend/coturn-service/turnserver.conf:/etc/coturn/turnserver.conf\n - coturn-logs:/var/log/coturn\n environment:\n - DETECT_EXTERNAL_IP=yes\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\nnetworks:\n chainlesschain-network:\n driver: bridge\n\nvolumes:\n ollama-data:\n qdrant-data:\n postgres-data:\n redis-data:\n whisper-models:\n coturn-logs:\n marketplace-minio-data:\n"}, "c3fe912fefd60bb6332748f59241fb6de61e9c5b": {"t": 1785754071.723048, "n": 10982, "h": "6e6830fa3a61fa19", "c": "version: '3.8'\n\nservices:\n # Whisper.cpp Local Speech Recognition Service (v0.27.0)\n # 高性能本地语音识别,支持 CPU 和 GPU (CUDA)\n whisper-service:\n build:\n context: ./desktop-app-vue/scripts/whisper-server\n dockerfile: Dockerfile\n args:\n - GPU_SUPPORT=${WHISPER_GPU:-cpu}\n image: chainlesschain/whisper-server:${WHISPER_GPU:-cpu}\n container_name: chainlesschain-whisper\n ports:\n - \"8002:8002\"\n environment:\n - WHISPER_MODEL_DIR=/app/models\n - WHISPER_MODEL_SIZE=${WHISPER_MODEL:-base}\n - WHISPER_SERVER_PORT=8002\n - WHISPER_DEVICE=${WHISPER_DEVICE:-auto}\n - WHISPER_MAX_FILE_SIZE=52428800\n volumes:\n - whisper-models:/app/models\n restart: unless-stopped\n healthcheck:\n test: [\"CMD-SHELL\", \"curl -f http://localhost:8002/health || exit 1\"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 60s\n networks:\n - chainlesschain-network\n # GPU 支持(需要 nvidia-container-toolkit)\n # deploy:\n # resources:\n # reservations:\n # devices:\n # - driver: nvidia\n # count: 1\n # capabilities: [gpu]\n\n # Ollama - 本地LLM服务(CPU版本)\n ollama:\n image: ollama/ollama:latest\n container_name: chainlesschain-ollama\n volumes:\n - ./data/ollama:/root/.ollama\n ports:\n - \"11434:11434\"\n environment:\n - OLLAMA_MODELS=/root/.ollama/models\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Qdrant - 向量数据库(后端AI服务使用)\n qdrant:\n image: qdrant/qdrant:latest\n container_name: chainlesschain-qdrant\n volumes:\n - ./data/qdrant:/qdrant/storage\n ports:\n - \"6333:6333\"\n - \"6334:6334\"\n environment:\n - QDRANT__SERVICE__GRPC_PORT=6334\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # ChromaDB - 向量数据库(Desktop App使用)\n chromadb:\n image: chromadb/chroma:latest\n container_name: chainlesschain-chromadb\n volumes:\n - ./data/chromadb:/chroma/chroma\n ports:\n - \"8000:8000\"\n environment:\n - IS_PERSISTENT=TRUE\n - ANONYMIZED_TELEMETRY=False\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # PostgreSQL - 项目元数据数据库\n postgres:\n image: postgres:16-alpine\n container_name: chainlesschain-postgres\n volumes:\n - ./data/postgres:/var/lib/postgresql/data\n # Note: Flyway handles migrations, don't use docker-entrypoint-initdb.d\n ports:\n - \"5432:5432\"\n environment:\n - POSTGRES_DB=chainlesschain\n - POSTGRES_USER=chainlesschain\n - POSTGRES_PASSWORD=chainlesschain_pwd_2024\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U chainlesschain -d chainlesschain\"]\n interval: 10s\n timeout: 5s\n retries: 5\n start_period: 10s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Redis - 缓存和消息队列\n redis:\n image: redis:7-alpine\n container_name: chainlesschain-redis\n volumes:\n - ./data/redis:/data\n ports:\n - \"6379:6379\"\n command: redis-server --appendonly yes --requirepass chainlesschain_redis_2024\n healthcheck:\n test: [\"CMD\", \"redis-cli\", \"--raw\", \"incr\", \"ping\"]\n interval: 10s\n timeout: 3s\n retries: 5\n start_period: 5s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # AI服务 - FastAPI Python服务\n ai-service:\n build:\n context: ./backend/ai-service\n dockerfile: Dockerfile\n container_name: chainlesschain-ai-service\n volumes:\n - ./backend/ai-service:/app\n - ./data/projects:/data/projects\n ports:\n - \"8001:8000\"\n env_file:\n - ./.env # 使用项目根目录的 .env 文件\n environment:\n # 基础服务连接\n - OLLAMA_HOST=http://ollama:11434\n - QDRANT_HOST=qdrant\n - QDRANT_PORT=6333\n - REDIS_HOST=redis\n - REDIS_PASSWORD=chainlesschain_redis_2024\n # HuggingFace镜像配置(解决国内网络访问问题)\n - HF_ENDPOINT=https://hf-mirror.com\n - TRANSFORMERS_OFFLINE=0\n # LLM配置 - 默认使用云LLM(dashscope),可在.env中配置API Key\n - LLM_PROVIDER=${LLM_PROVIDER:-dashscope}\n - LLM_MODEL=${LLM_MODEL:-qwen-turbo}\n # 云LLM API Keys(从.env文件读取)\n - DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY:-}\n - OPENAI_API_KEY=${OPENAI_API_KEY:-}\n - ZHIPU_API_KEY=${ZHIPU_API_KEY:-}\n - VOLCENGINE_API_KEY=${VOLCENGINE_API_KEY:-}\n - QIANFAN_API_KEY=${QIANFAN_API_KEY:-}\n - HUNYUAN_API_KEY=${HUNYUAN_API_KEY:-}\n - SPARK_API_KEY=${SPARK_API_KEY:-}\n - MINIMAX_API_KEY=${MINIMAX_API_KEY:-}\n - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY:-}\n depends_on:\n ollama:\n condition: service_started\n qdrant:\n condition: service_started\n redis:\n condition: service_healthy\n healthcheck:\n test: [\"CMD-SHELL\", \"python -c 'import urllib.request; urllib.request.urlopen(\\\"http://localhost:8000/health\\\")' || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 5\n start_period: 30s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 项目服务 - Spring Boot服务\n project-service:\n build:\n context: ./backend/project-service\n dockerfile: Dockerfile\n container_name: chainlesschain-project-service\n volumes:\n - ./data/projects:/data/projects\n ports:\n - \"9090:9090\"\n environment:\n # Spring Boot标准环境变量\n - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/chainlesschain\n - SPRING_DATASOURCE_USERNAME=chainlesschain\n - SPRING_DATASOURCE_PASSWORD=chainlesschain_pwd_2024\n - SPRING_DATA_REDIS_HOST=redis\n - SPRING_DATA_REDIS_PORT=6379\n - SPRING_DATA_REDIS_PASSWORD=chainlesschain_redis_2024\n # 自定义环境变量(application.yml中使用)\n - DB_HOST=postgres\n - DB_PORT=5432\n - DB_NAME=chainlesschain\n - DB_USER=chainlesschain\n - DB_PASSWORD=chainlesschain_pwd_2024\n - REDIS_HOST=redis\n - REDIS_PORT=6379\n - REDIS_PASSWORD=chainlesschain_redis_2024\n - AI_SERVICE_URL=http://ai-service:8000\n depends_on:\n postgres:\n condition: service_healthy\n redis:\n condition: service_healthy\n ai-service:\n condition: service_started\n healthcheck:\n test: [\"CMD-SHELL\", \"wget --no-verbose --tries=1 --spider http://localhost:9090/actuator/health || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 5\n start_period: 60s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 信令服务器 - WebSocket信令服务(移动端与PC端P2P通讯)\n signaling-server:\n build:\n context: ./signaling-server\n dockerfile: Dockerfile\n container_name: chainlesschain-signaling-server\n ports:\n - \"9001:9001\"\n - \"9002:9002\"\n environment:\n - PORT=9001\n - HEALTH_PORT=9002\n healthcheck:\n test: [\"CMD-SHELL\", \"wget --no-verbose --tries=1 --spider http://localhost:9002/health || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 3\n start_period: 10s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # MinIO - 插件市场对象存储(plugin .zip 包)\n marketplace-minio:\n image: minio/minio:latest\n container_name: chainlesschain-marketplace-minio\n command: server /data --console-address \":9011\"\n environment:\n - MINIO_ROOT_USER=${MARKETPLACE_MINIO_ACCESS_KEY:-minioadmin}\n - MINIO_ROOT_PASSWORD=${MARKETPLACE_MINIO_SECRET_KEY:-minioadmin123}\n ports:\n - \"9000:9000\" # S3 API\n - \"9011:9011\" # Console (9001 已被 signaling-server 占用)\n volumes:\n - ./data/marketplace-minio:/data\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:9000/minio/health/live\"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 20s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 插件市场数据库初始化(一次性,幂等)\n marketplace-db-init:\n image: postgres:16-alpine\n container_name: chainlesschain-marketplace-db-init\n depends_on:\n postgres:\n condition: service_healthy\n environment:\n - PGHOST=postgres\n - PGUSER=chainlesschain\n - PGPASSWORD=chainlesschain_pwd_2024\n - DB_NAME=plugin_marketplace\n - SCHEMA_FILE=/schema.sql\n volumes:\n - ./backend/plugin-marketplace-service/src/main/resources/schema.sql:/schema.sql:ro\n - ./deploy/init-marketplace-db.sh:/init.sh:ro\n entrypoint: [\"sh\", \"/init.sh\"]\n networks:\n - chainlesschain-network\n restart: \"no\"\n\n # 插件市场服务 - Spring Boot\n plugin-marketplace-service:\n build:\n context: ./backend/plugin-marketplace-service\n dockerfile: Dockerfile\n image: chainlesschain/plugin-marketplace-service:1.0.0\n container_name: chainlesschain-plugin-marketplace\n ports:\n - \"8090:8090\"\n environment:\n - SPRING_PROFILES_ACTIVE=prod\n # 复用根 postgres,使用独立库 plugin_marketplace\n - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/plugin_marketplace\n - SPRING_DATASOURCE_USERNAME=chainlesschain\n - SPRING_DATASOURCE_PASSWORD=chainlesschain_pwd_2024\n # 复用根 redis,用 database 2 避免与 project-service(0) 冲突\n - SPRING_DATA_REDIS_HOST=redis\n - SPRING_DATA_REDIS_PORT=6379\n - SPRING_DATA_REDIS_PASSWORD=chainlesschain_redis_2024\n - SPRING_DATA_REDIS_DATABASE=2\n # MinIO(专属,不复用其它服务)\n - MINIO_ENDPOINT=http://marketplace-minio:9000\n - MINIO_ACCESS_KEY=${MARKETPLACE_MINIO_ACCESS_KEY:-minioadmin}\n - MINIO_SECRET_KEY=${MARKETPLACE_MINIO_SECRET_KEY:-minioadmin123}\n - MINIO_BUCKET=plugins\n # JWT 必须在 .env 覆盖,默认值仅用于本地启动\n - JWT_SECRET=${PLUGIN_MARKETPLACE_JWT_SECRET:-CHANGE_ME_IN_PROD_at_least_256_bits_required_xxxxxxxxxxxxxxxxxx}\n - JWT_EXPIRATION=86400000\n - SERVER_PORT=8090\n - JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseG1GC\n depends_on:\n postgres:\n condition: service_healthy\n redis:\n condition: service_healthy\n marketplace-minio:\n condition: service_healthy\n marketplace-db-init:\n condition: service_completed_successfully\n healthcheck:\n test: [\"CMD-SHELL\", \"curl -f http://localhost:8090/api/actuator/health || exit 1\"]\n interval: 30s\n timeout: 10s\n retries: 5\n start_period: 90s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Coturn - STUN/TURN服务器(P2P NAT穿透)\n coturn:\n build:\n context: ./backend/coturn-service\n dockerfile: Dockerfile\n container_name: chainlesschain-coturn\n ports:\n - \"3478:3478\"\n - \"3478:3478/udp\"\n - \"5349:5349\"\n - \"5349:5349/udp\"\n - \"49152-49252:49152-49252/udp\" # TURN relay端口范围(限制为100个端口)\n volumes:\n - ./backend/coturn-service/turnserver.conf:/etc/coturn/turnserver.conf\n - coturn-logs:/var/log/coturn\n environment:\n - DETECT_EXTERNAL_IP=yes\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\nnetworks:\n chainlesschain-network:\n driver: bridge\n\nvolumes:\n ollama-data:\n qdrant-data:\n postgres-data:\n redis-data:\n whisper-models:\n coturn-logs:\n marketplace-minio-data:\n"}, "e2612b2f15916b59ffdb3c21a308f1a0e97bb940": {"t": 1785754074.2130177, "n": 10982, "h": "6e6830fa3a61fa19", "c": "version: '3.8'\n\nservices:\n # Whisper.cpp Local Speech Recognition Service (v0.27.0)\n # 高性能本地语音识别,支持 CPU 和 GPU (CUDA)\n whisper-service:\n build:\n context: ./desktop-app-vue/scripts/whisper-server\n dockerfile: Dockerfile\n args:\n - GPU_SUPPORT=${WHISPER_GPU:-cpu}\n image: chainlesschain/whisper-server:${WHISPER_GPU:-cpu}\n container_name: chainlesschain-whisper\n ports:\n - \"8002:8002\"\n environment:\n - WHISPER_MODEL_DIR=/app/models\n - WHISPER_MODEL_SIZE=${WHISPER_MODEL:-base}\n - WHISPER_SERVER_PORT=8002\n - WHISPER_DEVICE=${WHISPER_DEVICE:-auto}\n - WHISPER_MAX_FILE_SIZE=52428800\n volumes:\n - whisper-models:/app/models\n restart: unless-stopped\n healthcheck:\n test: [\"CMD-SHELL\", \"curl -f http://localhost:8002/health || exit 1\"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 60s\n networks:\n - chainlesschain-network\n # GPU 支持(需要 nvidia-container-toolkit)\n # deploy:\n # resources:\n # reservations:\n # devices:\n # - driver: nvidia\n # count: 1\n # capabilities: [gpu]\n\n # Ollama - 本地LLM服务(CPU版本)\n ollama:\n image: ollama/ollama:latest\n container_name: chainlesschain-ollama\n volumes:\n - ./data/ollama:/root/.ollama\n ports:\n - \"11434:11434\"\n environment:\n - OLLAMA_MODELS=/root/.ollama/models\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Qdrant - 向量数据库(后端AI服务使用)\n qdrant:\n image: qdrant/qdrant:latest\n container_name: chainlesschain-qdrant\n volumes:\n - ./data/qdrant:/qdrant/storage\n ports:\n - \"6333:6333\"\n - \"6334:6334\"\n environment:\n - QDRANT__SERVICE__GRPC_PORT=6334\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # ChromaDB - 向量数据库(Desktop App使用)\n chromadb:\n image: chromadb/chroma:latest\n container_name: chainlesschain-chromadb\n volumes:\n - ./data/chromadb:/chroma/chroma\n ports:\n - \"8000:8000\"\n environment:\n - IS_PERSISTENT=TRUE\n - ANONYMIZED_TELEMETRY=False\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # PostgreSQL - 项目元数据数据库\n postgres:\n image: postgres:16-alpine\n container_name: chainlesschain-postgres\n volumes:\n - ./data/postgres:/var/lib/postgresql/data\n # Note: Flyway handles migrations, don't use docker-entrypoint-initdb.d\n ports:\n - \"5432:5432\"\n environment:\n - POSTGRES_DB=chainlesschain\n - POSTGRES_USER=chainlesschain\n - POSTGRES_PASSWORD=chainlesschain_pwd_2024\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U chainlesschain -d chainlesschain\"]\n interval: 10s\n timeout: 5s\n retries: 5\n start_period: 10s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Redis - 缓存和消息队列\n redis:\n image: redis:7-alpine\n container_name: chainlesschain-redis\n volumes:\n - ./data/redis:/data\n ports:\n - \"6379:6379\"\n command: redis-server --appendonly yes --requirepass chainlesschain_redis_2024\n healthcheck:\n test: [\"CMD\", \"redis-cli\", \"--raw\", \"incr\", \"ping\"]\n interval: 10s\n timeout: 3s\n retries: 5\n start_period: 5s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # AI服务 - FastAPI Python服务\n ai-service:\n build:\n context: ./backend/ai-service\n dockerfile: Dockerfile\n container_name: chainlesschain-ai-service\n volumes:\n - ./backend/ai-service:/app\n - ./data/projects:/data/projects\n ports:\n - \"8001:8000\"\n env_file:\n - ./.env # 使用项目根目录的 .env 文件\n environment:\n # 基础服务连接\n - OLLAMA_HOST=http://ollama:11434\n - QDRANT_HOST=qdrant\n - QDRANT_PORT=6333\n - REDIS_HOST=redis\n - REDIS_PASSWORD=chainlesschain_redis_2024\n # HuggingFace镜像配置(解决国内网络访问问题)\n - HF_ENDPOINT=https://hf-mirror.com\n - TRANSFORMERS_OFFLINE=0\n # LLM配置 - 默认使用云LLM(dashscope),可在.env中配置API Key\n - LLM_PROVIDER=${LLM_PROVIDER:-dashscope}\n - LLM_MODEL=${LLM_MODEL:-qwen-turbo}\n # 云LLM API Keys(从.env文件读取)\n - DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY:-}\n - OPENAI_API_KEY=${OPENAI_API_KEY:-}\n - ZHIPU_API_KEY=${ZHIPU_API_KEY:-}\n - VOLCENGINE_API_KEY=${VOLCENGINE_API_KEY:-}\n - QIANFAN_API_KEY=${QIANFAN_API_KEY:-}\n - HUNYUAN_API_KEY=${HUNYUAN_API_KEY:-}\n - SPARK_API_KEY=${SPARK_API_KEY:-}\n - MINIMAX_API_KEY=${MINIMAX_API_KEY:-}\n - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY:-}\n depends_on:\n ollama:\n condition: service_started\n qdrant:\n condition: service_started\n redis:\n condition: service_healthy\n healthcheck:\n test: [\"CMD-SHELL\", \"python -c 'import urllib.request; urllib.request.urlopen(\\\"http://localhost:8000/health\\\")' || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 5\n start_period: 30s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 项目服务 - Spring Boot服务\n project-service:\n build:\n context: ./backend/project-service\n dockerfile: Dockerfile\n container_name: chainlesschain-project-service\n volumes:\n - ./data/projects:/data/projects\n ports:\n - \"9090:9090\"\n environment:\n # Spring Boot标准环境变量\n - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/chainlesschain\n - SPRING_DATASOURCE_USERNAME=chainlesschain\n - SPRING_DATASOURCE_PASSWORD=chainlesschain_pwd_2024\n - SPRING_DATA_REDIS_HOST=redis\n - SPRING_DATA_REDIS_PORT=6379\n - SPRING_DATA_REDIS_PASSWORD=chainlesschain_redis_2024\n # 自定义环境变量(application.yml中使用)\n - DB_HOST=postgres\n - DB_PORT=5432\n - DB_NAME=chainlesschain\n - DB_USER=chainlesschain\n - DB_PASSWORD=chainlesschain_pwd_2024\n - REDIS_HOST=redis\n - REDIS_PORT=6379\n - REDIS_PASSWORD=chainlesschain_redis_2024\n - AI_SERVICE_URL=http://ai-service:8000\n depends_on:\n postgres:\n condition: service_healthy\n redis:\n condition: service_healthy\n ai-service:\n condition: service_started\n healthcheck:\n test: [\"CMD-SHELL\", \"wget --no-verbose --tries=1 --spider http://localhost:9090/actuator/health || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 5\n start_period: 60s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 信令服务器 - WebSocket信令服务(移动端与PC端P2P通讯)\n signaling-server:\n build:\n context: ./signaling-server\n dockerfile: Dockerfile\n container_name: chainlesschain-signaling-server\n ports:\n - \"9001:9001\"\n - \"9002:9002\"\n environment:\n - PORT=9001\n - HEALTH_PORT=9002\n healthcheck:\n test: [\"CMD-SHELL\", \"wget --no-verbose --tries=1 --spider http://localhost:9002/health || exit 1\"]\n interval: 15s\n timeout: 5s\n retries: 3\n start_period: 10s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # MinIO - 插件市场对象存储(plugin .zip 包)\n marketplace-minio:\n image: minio/minio:latest\n container_name: chainlesschain-marketplace-minio\n command: server /data --console-address \":9011\"\n environment:\n - MINIO_ROOT_USER=${MARKETPLACE_MINIO_ACCESS_KEY:-minioadmin}\n - MINIO_ROOT_PASSWORD=${MARKETPLACE_MINIO_SECRET_KEY:-minioadmin123}\n ports:\n - \"9000:9000\" # S3 API\n - \"9011:9011\" # Console (9001 已被 signaling-server 占用)\n volumes:\n - ./data/marketplace-minio:/data\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:9000/minio/health/live\"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 20s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # 插件市场数据库初始化(一次性,幂等)\n marketplace-db-init:\n image: postgres:16-alpine\n container_name: chainlesschain-marketplace-db-init\n depends_on:\n postgres:\n condition: service_healthy\n environment:\n - PGHOST=postgres\n - PGUSER=chainlesschain\n - PGPASSWORD=chainlesschain_pwd_2024\n - DB_NAME=plugin_marketplace\n - SCHEMA_FILE=/schema.sql\n volumes:\n - ./backend/plugin-marketplace-service/src/main/resources/schema.sql:/schema.sql:ro\n - ./deploy/init-marketplace-db.sh:/init.sh:ro\n entrypoint: [\"sh\", \"/init.sh\"]\n networks:\n - chainlesschain-network\n restart: \"no\"\n\n # 插件市场服务 - Spring Boot\n plugin-marketplace-service:\n build:\n context: ./backend/plugin-marketplace-service\n dockerfile: Dockerfile\n image: chainlesschain/plugin-marketplace-service:1.0.0\n container_name: chainlesschain-plugin-marketplace\n ports:\n - \"8090:8090\"\n environment:\n - SPRING_PROFILES_ACTIVE=prod\n # 复用根 postgres,使用独立库 plugin_marketplace\n - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/plugin_marketplace\n - SPRING_DATASOURCE_USERNAME=chainlesschain\n - SPRING_DATASOURCE_PASSWORD=chainlesschain_pwd_2024\n # 复用根 redis,用 database 2 避免与 project-service(0) 冲突\n - SPRING_DATA_REDIS_HOST=redis\n - SPRING_DATA_REDIS_PORT=6379\n - SPRING_DATA_REDIS_PASSWORD=chainlesschain_redis_2024\n - SPRING_DATA_REDIS_DATABASE=2\n # MinIO(专属,不复用其它服务)\n - MINIO_ENDPOINT=http://marketplace-minio:9000\n - MINIO_ACCESS_KEY=${MARKETPLACE_MINIO_ACCESS_KEY:-minioadmin}\n - MINIO_SECRET_KEY=${MARKETPLACE_MINIO_SECRET_KEY:-minioadmin123}\n - MINIO_BUCKET=plugins\n # JWT 必须在 .env 覆盖,默认值仅用于本地启动\n - JWT_SECRET=${PLUGIN_MARKETPLACE_JWT_SECRET:-CHANGE_ME_IN_PROD_at_least_256_bits_required_xxxxxxxxxxxxxxxxxx}\n - JWT_EXPIRATION=86400000\n - SERVER_PORT=8090\n - JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseG1GC\n depends_on:\n postgres:\n condition: service_healthy\n redis:\n condition: service_healthy\n marketplace-minio:\n condition: service_healthy\n marketplace-db-init:\n condition: service_completed_successfully\n healthcheck:\n test: [\"CMD-SHELL\", \"curl -f http://localhost:8090/api/actuator/health || exit 1\"]\n interval: 30s\n timeout: 10s\n retries: 5\n start_period: 90s\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\n # Coturn - STUN/TURN服务器(P2P NAT穿透)\n coturn:\n build:\n context: ./backend/coturn-service\n dockerfile: Dockerfile\n container_name: chainlesschain-coturn\n ports:\n - \"3478:3478\"\n - \"3478:3478/udp\"\n - \"5349:5349\"\n - \"5349:5349/udp\"\n - \"49152-49252:49152-49252/udp\" # TURN relay端口范围(限制为100个端口)\n volumes:\n - ./backend/coturn-service/turnserver.conf:/etc/coturn/turnserver.conf\n - coturn-logs:/var/log/coturn\n environment:\n - DETECT_EXTERNAL_IP=yes\n restart: unless-stopped\n networks:\n - chainlesschain-network\n\nnetworks:\n chainlesschain-network:\n driver: bridge\n\nvolumes:\n ollama-data:\n qdrant-data:\n postgres-data:\n redis-data:\n whisper-models:\n coturn-logs:\n marketplace-minio-data:\n"}}, "backend/ai-service/README_LLM_CONFIG.md": {"19dcdea87a87892fe9eb22a23b4f3fe9ce05af93": {"t": 1785747258.0519283, "n": 2592, "h": "18af5f658babac49", "c": "# LLM配置指南\n\n## 概述\n\nChainlessChain AI服务默认优先使用**云LLM**(阿里云DashScope通义千问),如果未配置API Key则自动回退到本地Ollama。\n\n## 智能Fallback机制\n\n1. **优先使用云LLM**:默认使用`dashscope`(通义千问)\n2. **自动回退**:如果云LLM的API Key未配置或为空,自动切换到本地Ollama\n3. **确保可用**:无论云LLM是否配置,服务始终可用\n\n## 配置步骤\n\n### 1. 编辑`.env`文件\n\n在`backend/ai-service/.env`文件中配置:\n\n```bash\n# 选择LLM提供商(dashscope推荐)\nLLM_PROVIDER=dashscope\nLLM_MODEL=qwen-turbo\n\n# 配置阿里云DashScope API Key\nDASHSCOPE_API_KEY=sk-your-api-key-here\n```\n\n### 2. 获取API Key\n\n#### 阿里云DashScope(推荐)\n- 访问:https://dashscope.console.aliyun.com/\n- 注册并创建API Key\n- 免费额度:每月300万tokens\n\n#### 其他支持的云LLM提供商\n\n| 提供商 | 环境变量 | 获取地址 |\n|--------|----------|----------|\n| OpenAI | `OPENAI_API_KEY` | https://platform.openai.com/ |\n| 智谱AI | `ZHIPU_API_KEY` | https://open.bigmodel.cn/ |\n| 百度千帆 | `QIANFAN_API_KEY` | https://cloud.baidu.com/product/wenxinworkshop |\n| 火山引擎 | `VOLCENGINE_API_KEY` | https://console.volcengine.com/ |\n| 腾讯混元 | `HUNYUAN_API_KEY` | https://cloud.tencent.com/product/hunyuan |\n| 讯飞星火 | `SPARK_API_KEY` | https://xinghuo.xfyun.cn/ |\n| MiniMax | `MINIMAX_API_KEY` | https://api.minimax.chat/ |\n| DeepSeek | `DEEPSEEK_API_KEY` | https://platform.deepseek.com/ |\n\n### 3. 重启服务\n\n```bash\n# 重启AI服务容器\ndocker-compose restart ai-service\n\n# 或重启所有服务\ndocker-compose down\ndocker-compose up -d\n```\n\n## 配置示例\n\n### 使用阿里云DashScope\n```bash\nLLM_PROVIDER=dashscope\nLLM_MODEL=qwen-turbo\nDASHSCOPE_API_KEY=sk-xxxxxxxxxxxxx\n```\n\n### 使用智谱AI\n```bash\nLLM_PROVIDER=zhipu\nLLM_MODEL=glm-4\nZHIPU_API_KEY=xxxxxxxxxxxxx\n```\n\n### 使用OpenAI\n```bash\nLLM_PROVIDER=openai\nLLM_MODEL=gpt-4o-mini\nOPENAI_API_KEY=sk-xxxxxxxxxxxxx\n```\n\n### 回退到本地Ollama\n```bash\nLLM_PROVIDER=ollama\nLLM_MODEL=qwen2:7b\n# 不需要API Key\n```\n\n## 验证配置\n\n### 1. 查看服务日志\n```bash\ndocker-compose logs -f ai-service\n```\n\n如果看到类似日志,说明自动回退成功:\n```\nWARNING: LLM_PROVIDER设置为'dashscope',但未配置API Key,自动回退到Ollama本地LLM\n```\n\n### 2. 测试API\n```bash\ncurl -X POST http://localhost:8001/api/projects/create \\\n -H \"Content-Type: application/json\" \\\n -d '{\"user_prompt\": \"创建一个简单的待办事项列表\", \"project_type\": \"web\"}'\n```\n\n## 常见问题\n\n### Q: 为什么推荐使用DashScope?\nA:\n- 国内访问速度快\n- 有免费额度\n- 支持中文效果好\n- API稳定可靠\n\n### Q: 如何查看当前使用的LLM?\nA: 查看AI服务日志,会显示当前使用的提供商和模型\n\n### Q: 云LLM和本地Ollama的区别?\nA:\n- **云LLM**:速度快、效果好、需要网络、需要API Key\n- **本地Ollama**:离线可用、免费、需要下载模型、速度取决于硬件\n\n### Q: 如何只使用本地Ollama?\nA: 将`.env`中的`LLM_PROVIDER`设置为`ollama`\n\n### Q: API Key如何保密?\nA: `.env`文件已在`.gitignore`中,不会被提交到Git仓库\n\n## 性能建议\n\n1. **生产环境**:推荐使用云LLM,速度快效果好\n2. **开发环境**:可以使用本地Ollama节省成本\n3. **离线场景**:使用本地Ollama\n4. **混合使用**:配置多个API Key,根据需要切换\n\n## 技术支持\n\n如有问题,请参考:\n- 项目主README:`../../README.md`\n- 快速开始:`../../QUICK_START.md`\n- 系统设计文档:`../../系统设计_个人移动AI管理系统.md`\n"}, "3b0b2dc09dceaf104b65bdb7bdc4448fc55e53a5": {"t": 1785754069.1913946, "n": 2592, "h": "18af5f658babac49", "c": "# LLM配置指南\n\n## 概述\n\nChainlessChain AI服务默认优先使用**云LLM**(阿里云DashScope通义千问),如果未配置API Key则自动回退到本地Ollama。\n\n## 智能Fallback机制\n\n1. **优先使用云LLM**:默认使用`dashscope`(通义千问)\n2. **自动回退**:如果云LLM的API Key未配置或为空,自动切换到本地Ollama\n3. **确保可用**:无论云LLM是否配置,服务始终可用\n\n## 配置步骤\n\n### 1. 编辑`.env`文件\n\n在`backend/ai-service/.env`文件中配置:\n\n```bash\n# 选择LLM提供商(dashscope推荐)\nLLM_PROVIDER=dashscope\nLLM_MODEL=qwen-turbo\n\n# 配置阿里云DashScope API Key\nDASHSCOPE_API_KEY=sk-your-api-key-here\n```\n\n### 2. 获取API Key\n\n#### 阿里云DashScope(推荐)\n- 访问:https://dashscope.console.aliyun.com/\n- 注册并创建API Key\n- 免费额度:每月300万tokens\n\n#### 其他支持的云LLM提供商\n\n| 提供商 | 环境变量 | 获取地址 |\n|--------|----------|----------|\n| OpenAI | `OPENAI_API_KEY` | https://platform.openai.com/ |\n| 智谱AI | `ZHIPU_API_KEY` | https://open.bigmodel.cn/ |\n| 百度千帆 | `QIANFAN_API_KEY` | https://cloud.baidu.com/product/wenxinworkshop |\n| 火山引擎 | `VOLCENGINE_API_KEY` | https://console.volcengine.com/ |\n| 腾讯混元 | `HUNYUAN_API_KEY` | https://cloud.tencent.com/product/hunyuan |\n| 讯飞星火 | `SPARK_API_KEY` | https://xinghuo.xfyun.cn/ |\n| MiniMax | `MINIMAX_API_KEY` | https://api.minimax.chat/ |\n| DeepSeek | `DEEPSEEK_API_KEY` | https://platform.deepseek.com/ |\n\n### 3. 重启服务\n\n```bash\n# 重启AI服务容器\ndocker-compose restart ai-service\n\n# 或重启所有服务\ndocker-compose down\ndocker-compose up -d\n```\n\n## 配置示例\n\n### 使用阿里云DashScope\n```bash\nLLM_PROVIDER=dashscope\nLLM_MODEL=qwen-turbo\nDASHSCOPE_API_KEY=sk-xxxxxxxxxxxxx\n```\n\n### 使用智谱AI\n```bash\nLLM_PROVIDER=zhipu\nLLM_MODEL=glm-4\nZHIPU_API_KEY=xxxxxxxxxxxxx\n```\n\n### 使用OpenAI\n```bash\nLLM_PROVIDER=openai\nLLM_MODEL=gpt-4o-mini\nOPENAI_API_KEY=sk-xxxxxxxxxxxxx\n```\n\n### 回退到本地Ollama\n```bash\nLLM_PROVIDER=ollama\nLLM_MODEL=qwen2:7b\n# 不需要API Key\n```\n\n## 验证配置\n\n### 1. 查看服务日志\n```bash\ndocker-compose logs -f ai-service\n```\n\n如果看到类似日志,说明自动回退成功:\n```\nWARNING: LLM_PROVIDER设置为'dashscope',但未配置API Key,自动回退到Ollama本地LLM\n```\n\n### 2. 测试API\n```bash\ncurl -X POST http://localhost:8001/api/projects/create \\\n -H \"Content-Type: application/json\" \\\n -d '{\"user_prompt\": \"创建一个简单的待办事项列表\", \"project_type\": \"web\"}'\n```\n\n## 常见问题\n\n### Q: 为什么推荐使用DashScope?\nA:\n- 国内访问速度快\n- 有免费额度\n- 支持中文效果好\n- API稳定可靠\n\n### Q: 如何查看当前使用的LLM?\nA: 查看AI服务日志,会显示当前使用的提供商和模型\n\n### Q: 云LLM和本地Ollama的区别?\nA:\n- **云LLM**:速度快、效果好、需要网络、需要API Key\n- **本地Ollama**:离线可用、免费、需要下载模型、速度取决于硬件\n\n### Q: 如何只使用本地Ollama?\nA: 将`.env`中的`LLM_PROVIDER`设置为`ollama`\n\n### Q: API Key如何保密?\nA: `.env`文件已在`.gitignore`中,不会被提交到Git仓库\n\n## 性能建议\n\n1. **生产环境**:推荐使用云LLM,速度快效果好\n2. **开发环境**:可以使用本地Ollama节省成本\n3. **离线场景**:使用本地Ollama\n4. **混合使用**:配置多个API Key,根据需要切换\n\n## 技术支持\n\n如有问题,请参考:\n- 项目主README:`../../README.md`\n- 快速开始:`../../QUICK_START.md`\n- 系统设计文档:`../../系统设计_个人移动AI管理系统.md`\n"}, "c3fe912fefd60bb6332748f59241fb6de61e9c5b": {"t": 1785754072.011146, "n": 2592, "h": "18af5f658babac49", "c": "# LLM配置指南\n\n## 概述\n\nChainlessChain AI服务默认优先使用**云LLM**(阿里云DashScope通义千问),如果未配置API Key则自动回退到本地Ollama。\n\n## 智能Fallback机制\n\n1. **优先使用云LLM**:默认使用`dashscope`(通义千问)\n2. **自动回退**:如果云LLM的API Key未配置或为空,自动切换到本地Ollama\n3. **确保可用**:无论云LLM是否配置,服务始终可用\n\n## 配置步骤\n\n### 1. 编辑`.env`文件\n\n在`backend/ai-service/.env`文件中配置:\n\n```bash\n# 选择LLM提供商(dashscope推荐)\nLLM_PROVIDER=dashscope\nLLM_MODEL=qwen-turbo\n\n# 配置阿里云DashScope API Key\nDASHSCOPE_API_KEY=sk-your-api-key-here\n```\n\n### 2. 获取API Key\n\n#### 阿里云DashScope(推荐)\n- 访问:https://dashscope.console.aliyun.com/\n- 注册并创建API Key\n- 免费额度:每月300万tokens\n\n#### 其他支持的云LLM提供商\n\n| 提供商 | 环境变量 | 获取地址 |\n|--------|----------|----------|\n| OpenAI | `OPENAI_API_KEY` | https://platform.openai.com/ |\n| 智谱AI | `ZHIPU_API_KEY` | https://open.bigmodel.cn/ |\n| 百度千帆 | `QIANFAN_API_KEY` | https://cloud.baidu.com/product/wenxinworkshop |\n| 火山引擎 | `VOLCENGINE_API_KEY` | https://console.volcengine.com/ |\n| 腾讯混元 | `HUNYUAN_API_KEY` | https://cloud.tencent.com/product/hunyuan |\n| 讯飞星火 | `SPARK_API_KEY` | https://xinghuo.xfyun.cn/ |\n| MiniMax | `MINIMAX_API_KEY` | https://api.minimax.chat/ |\n| DeepSeek | `DEEPSEEK_API_KEY` | https://platform.deepseek.com/ |\n\n### 3. 重启服务\n\n```bash\n# 重启AI服务容器\ndocker-compose restart ai-service\n\n# 或重启所有服务\ndocker-compose down\ndocker-compose up -d\n```\n\n## 配置示例\n\n### 使用阿里云DashScope\n```bash\nLLM_PROVIDER=dashscope\nLLM_MODEL=qwen-turbo\nDASHSCOPE_API_KEY=sk-xxxxxxxxxxxxx\n```\n\n### 使用智谱AI\n```bash\nLLM_PROVIDER=zhipu\nLLM_MODEL=glm-4\nZHIPU_API_KEY=xxxxxxxxxxxxx\n```\n\n### 使用OpenAI\n```bash\nLLM_PROVIDER=openai\nLLM_MODEL=gpt-4o-mini\nOPENAI_API_KEY=sk-xxxxxxxxxxxxx\n```\n\n### 回退到本地Ollama\n```bash\nLLM_PROVIDER=ollama\nLLM_MODEL=qwen2:7b\n# 不需要API Key\n```\n\n## 验证配置\n\n### 1. 查看服务日志\n```bash\ndocker-compose logs -f ai-service\n```\n\n如果看到类似日志,说明自动回退成功:\n```\nWARNING: LLM_PROVIDER设置为'dashscope',但未配置API Key,自动回退到Ollama本地LLM\n```\n\n### 2. 测试API\n```bash\ncurl -X POST http://localhost:8001/api/projects/create \\\n -H \"Content-Type: application/json\" \\\n -d '{\"user_prompt\": \"创建一个简单的待办事项列表\", \"project_type\": \"web\"}'\n```\n\n## 常见问题\n\n### Q: 为什么推荐使用DashScope?\nA:\n- 国内访问速度快\n- 有免费额度\n- 支持中文效果好\n- API稳定可靠\n\n### Q: 如何查看当前使用的LLM?\nA: 查看AI服务日志,会显示当前使用的提供商和模型\n\n### Q: 云LLM和本地Ollama的区别?\nA:\n- **云LLM**:速度快、效果好、需要网络、需要API Key\n- **本地Ollama**:离线可用、免费、需要下载模型、速度取决于硬件\n\n### Q: 如何只使用本地Ollama?\nA: 将`.env`中的`LLM_PROVIDER`设置为`ollama`\n\n### Q: API Key如何保密?\nA: `.env`文件已在`.gitignore`中,不会被提交到Git仓库\n\n## 性能建议\n\n1. **生产环境**:推荐使用云LLM,速度快效果好\n2. **开发环境**:可以使用本地Ollama节省成本\n3. **离线场景**:使用本地Ollama\n4. **混合使用**:配置多个API Key,根据需要切换\n\n## 技术支持\n\n如有问题,请参考:\n- 项目主README:`../../README.md`\n- 快速开始:`../../QUICK_START.md`\n- 系统设计文档:`../../系统设计_个人移动AI管理系统.md`\n"}, "e2612b2f15916b59ffdb3c21a308f1a0e97bb940": {"t": 1785754074.424818, "n": 2592, "h": "18af5f658babac49", "c": "# LLM配置指南\n\n## 概述\n\nChainlessChain AI服务默认优先使用**云LLM**(阿里云DashScope通义千问),如果未配置API Key则自动回退到本地Ollama。\n\n## 智能Fallback机制\n\n1. **优先使用云LLM**:默认使用`dashscope`(通义千问)\n2. **自动回退**:如果云LLM的API Key未配置或为空,自动切换到本地Ollama\n3. **确保可用**:无论云LLM是否配置,服务始终可用\n\n## 配置步骤\n\n### 1. 编辑`.env`文件\n\n在`backend/ai-service/.env`文件中配置:\n\n```bash\n# 选择LLM提供商(dashscope推荐)\nLLM_PROVIDER=dashscope\nLLM_MODEL=qwen-turbo\n\n# 配置阿里云DashScope API Key\nDASHSCOPE_API_KEY=sk-your-api-key-here\n```\n\n### 2. 获取API Key\n\n#### 阿里云DashScope(推荐)\n- 访问:https://dashscope.console.aliyun.com/\n- 注册并创建API Key\n- 免费额度:每月300万tokens\n\n#### 其他支持的云LLM提供商\n\n| 提供商 | 环境变量 | 获取地址 |\n|--------|----------|----------|\n| OpenAI | `OPENAI_API_KEY` | https://platform.openai.com/ |\n| 智谱AI | `ZHIPU_API_KEY` | https://open.bigmodel.cn/ |\n| 百度千帆 | `QIANFAN_API_KEY` | https://cloud.baidu.com/product/wenxinworkshop |\n| 火山引擎 | `VOLCENGINE_API_KEY` | https://console.volcengine.com/ |\n| 腾讯混元 | `HUNYUAN_API_KEY` | https://cloud.tencent.com/product/hunyuan |\n| 讯飞星火 | `SPARK_API_KEY` | https://xinghuo.xfyun.cn/ |\n| MiniMax | `MINIMAX_API_KEY` | https://api.minimax.chat/ |\n| DeepSeek | `DEEPSEEK_API_KEY` | https://platform.deepseek.com/ |\n\n### 3. 重启服务\n\n```bash\n# 重启AI服务容器\ndocker-compose restart ai-service\n\n# 或重启所有服务\ndocker-compose down\ndocker-compose up -d\n```\n\n## 配置示例\n\n### 使用阿里云DashScope\n```bash\nLLM_PROVIDER=dashscope\nLLM_MODEL=qwen-turbo\nDASHSCOPE_API_KEY=sk-xxxxxxxxxxxxx\n```\n\n### 使用智谱AI\n```bash\nLLM_PROVIDER=zhipu\nLLM_MODEL=glm-4\nZHIPU_API_KEY=xxxxxxxxxxxxx\n```\n\n### 使用OpenAI\n```bash\nLLM_PROVIDER=openai\nLLM_MODEL=gpt-4o-mini\nOPENAI_API_KEY=sk-xxxxxxxxxxxxx\n```\n\n### 回退到本地Ollama\n```bash\nLLM_PROVIDER=ollama\nLLM_MODEL=qwen2:7b\n# 不需要API Key\n```\n\n## 验证配置\n\n### 1. 查看服务日志\n```bash\ndocker-compose logs -f ai-service\n```\n\n如果看到类似日志,说明自动回退成功:\n```\nWARNING: LLM_PROVIDER设置为'dashscope',但未配置API Key,自动回退到Ollama本地LLM\n```\n\n### 2. 测试API\n```bash\ncurl -X POST http://localhost:8001/api/projects/create \\\n -H \"Content-Type: application/json\" \\\n -d '{\"user_prompt\": \"创建一个简单的待办事项列表\", \"project_type\": \"web\"}'\n```\n\n## 常见问题\n\n### Q: 为什么推荐使用DashScope?\nA:\n- 国内访问速度快\n- 有免费额度\n- 支持中文效果好\n- API稳定可靠\n\n### Q: 如何查看当前使用的LLM?\nA: 查看AI服务日志,会显示当前使用的提供商和模型\n\n### Q: 云LLM和本地Ollama的区别?\nA:\n- **云LLM**:速度快、效果好、需要网络、需要API Key\n- **本地Ollama**:离线可用、免费、需要下载模型、速度取决于硬件\n\n### Q: 如何只使用本地Ollama?\nA: 将`.env`中的`LLM_PROVIDER`设置为`ollama`\n\n### Q: API Key如何保密?\nA: `.env`文件已在`.gitignore`中,不会被提交到Git仓库\n\n## 性能建议\n\n1. **生产环境**:推荐使用云LLM,速度快效果好\n2. **开发环境**:可以使用本地Ollama节省成本\n3. **离线场景**:使用本地Ollama\n4. **混合使用**:配置多个API Key,根据需要切换\n\n## 技术支持\n\n如有问题,请参考:\n- 项目主README:`../../README.md`\n- 快速开始:`../../QUICK_START.md`\n- 系统设计文档:`../../系统设计_个人移动AI管理系统.md`\n"}}, "desktop-app-vue/src/main/llm/secure-config-storage.js": {"e8e7ba274b487ed491c04ec3359841a0e545debb": {"t": 1785809859.9925716, "n": 23864, "h": "e8b0bc5ff1328f47", "c": "/**\n * 安全配置存储模块\n *\n * 提供三层加密策略:\n * 1. Electron safeStorage (操作系统凭证存储 - 最安全)\n * 2. AES-256-GCM + 机器特征派生密钥 (后备方案)\n * 3. 基于密码的加密 (用于导出/迁移)\n *\n * @module secure-config-storage\n */\n\nconst { logger } = require(\"../utils/logger.js\");\nconst crypto = require(\"crypto\");\nconst fs = require(\"fs\");\nconst fsp = require(\"fs\").promises;\nconst path = require(\"path\");\nconst { app, safeStorage } = require(\"electron\");\nconst os = require(\"os\");\n\n// 加密算法配置\nconst ALGORITHM = \"aes-256-gcm\";\nconst IV_LENGTH = 16;\nconst AUTH_TAG_LENGTH = 16;\nconst SALT_LENGTH = 32;\nconst KEY_LENGTH = 32;\nconst ITERATIONS = 100000;\n\n// 存储版本(用于未来迁移)\nconst STORAGE_VERSION = 2;\n\n/**\n * 敏感配置字段列表 - 支持14+个LLM提供商\n */\nconst SENSITIVE_FIELDS = [\n // 主要国际提供商\n \"openai.apiKey\",\n \"anthropic.apiKey\",\n \"deepseek.apiKey\",\n \"google.apiKey\", // Google AI / Gemini\n \"cohere.apiKey\", // Cohere\n \"mistral.apiKey\", // Mistral AI\n\n // 国内主要提供商\n \"volcengine.apiKey\", // 豆包(火山引擎)\n \"dashscope.apiKey\", // 阿里云通义千问\n \"zhipuai.apiKey\", // 智谱AI GLM\n \"baidu.apiKey\", // 百度千帆\n \"baidu.secretKey\", // 百度千帆 Secret Key\n \"moonshot.apiKey\", // 月之暗面 Kimi\n \"minimax.apiKey\", // MiniMax\n \"minimax.groupId\", // MiniMax Group ID\n \"xunfei.apiKey\", // 讯飞星火\n \"xunfei.apiSecret\", // 讯飞星火 Secret\n \"xunfei.appId\", // 讯飞星火 App ID\n \"stepfun.apiKey\", // 阶跃星辰\n \"yi.apiKey\", // 零一万物\n \"baichuan.apiKey\", // 百川智能\n \"sensenova.apiKey\", // 商汤日日新\n\n // 自定义和代理\n \"custom.apiKey\",\n \"custom.apiSecret\",\n \"proxy.password\", // 代理认证密码\n\n // 嵌入服务\n \"embedding.apiKey\",\n \"embedding.secretKey\",\n\n // MCP服务器凭证\n \"mcp.postgres.password\",\n \"mcp.mysql.password\",\n \"mcp.redis.password\",\n\n // 外部同步 provider 凭证(Phase 3c — WebDAV / S3 / OSS)\n // 整个 sync..* 都进 secure-config.enc 加密 blob,但只有\n // password / secret 类字段进 SENSITIVE_FIELDS(控制 sanitizeConfig\n // 渲染端 mask 行为;URL / username / remotePath 仍以明文返回给前端展示)\n \"sync.webdav.password\",\n \"sync.oss.secretAccessKey\",\n];\n\n/**\n * API Key 格式验证规则\n */\nconst API_KEY_PATTERNS = {\n openai: /^sk-[a-zA-Z0-9]{32,}$/,\n anthropic: /^sk-ant-[a-zA-Z0-9-]{32,}$/,\n deepseek: /^sk-[a-zA-Z0-9]{32,}$/,\n google: /^AIza[a-zA-Z0-9_-]{32,}$/,\n cohere: /^[a-zA-Z0-9]{32,}$/,\n volcengine: /^[a-f0-9-]{36}$/, // UUID格式\n dashscope: /^sk-[a-zA-Z0-9]{32,}$/,\n zhipuai: /^[a-zA-Z0-9]{32,}\\.[a-zA-Z0-9]+$/, // 格式: key.secret\n moonshot: /^sk-[a-zA-Z0-9]{32,}$/,\n};\n\n/**\n * 安全配置存储类\n *\n * 使用策略模式支持多种加密后端:\n * - safeStorage: Electron原生,使用操作系统凭证管理器\n * - fallback: 基于机器特征的AES-256-GCM加密\n */\nclass SecureConfigStorage {\n constructor(options = {}) {\n // Injectable Electron app / safeStorage (dependency injection) so unit tests\n // can supply mocks. vi.mock(\"electron\") does not reach this module's\n // CommonJS `require(\"electron\")` destructure in Vitest's forks pool, leaving\n // `app`/`safeStorage` undefined there. Defaults to the real Electron exports.\n this._app = options.app || app;\n this._safeStorage = options.safeStorage || safeStorage;\n this.storagePath = options.storagePath || this._getDefaultStoragePath();\n this.safeStorageAvailable = this._checkSafeStorageAvailability();\n this.machineKey = null;\n this._cache = null;\n this._cacheTimestamp = null;\n this._cacheTTL = 5 * 60 * 1000; // 5分钟缓存\n }\n\n /**\n * 检查 Electron safeStorage 是否可用\n * @private\n */\n _checkSafeStorageAvailability() {\n try {\n if (\n this._safeStorage &&\n typeof this._safeStorage.isEncryptionAvailable === \"function\"\n ) {\n const available = this._safeStorage.isEncryptionAvailable();\n logger.info(`[SecureConfigStorage] safeStorage 可用性: ${available}`);\n return available;\n }\n } catch (error) {\n logger.warn(\"[SecureConfigStorage] safeStorage 检查失败:\", error.message);\n }\n return false;\n }\n\n /**\n * 获取默认存储路径\n * @private\n */\n _getDefaultStoragePath() {\n const userDataPath = this._app.getPath(\"userData\");\n return path.join(userDataPath, \"secure-config.enc\");\n }\n\n /**\n * 获取备份目录路径\n * @private\n */\n _getBackupDir() {\n const userDataPath = this._app.getPath(\"userData\");\n return path.join(userDataPath, \"secure-backups\");\n }\n\n /**\n * 获取机器特定的密钥种子\n * @private\n */\n _getMachineKeySeed() {\n const components = [\n os.hostname(),\n os.platform(),\n os.arch(),\n os.homedir(),\n JSON.stringify(\n os\n .cpus()\n .map((c) => c.model)\n .slice(0, 1),\n ),\n \"chainlesschain-llm-config-v2\",\n ];\n return components.join(\"|\");\n }\n\n /**\n * 从机器种子派生加密密钥\n * @private\n */\n _deriveKey(salt) {\n const seed = this._getMachineKeySeed();\n return crypto.pbkdf2Sync(seed, salt, ITERATIONS, KEY_LENGTH, \"sha256\");\n }\n\n /**\n * 使用 safeStorage 加密数据\n * @private\n */\n _encryptWithSafeStorage(data) {\n const jsonData = JSON.stringify({\n version: STORAGE_VERSION,\n data: data,\n timestamp: Date.now(),\n });\n const encrypted = this._safeStorage.encryptString(jsonData);\n\n // 添加标记头以识别加密方式\n const header = Buffer.from([0x53, 0x53, STORAGE_VERSION]); // 'SS' + version\n return Buffer.concat([header, encrypted]);\n }\n\n /**\n * 使用 safeStorage 解密数据\n * @private\n */\n _decryptWithSafeStorage(encryptedData) {\n // 跳过标记头\n const encrypted = encryptedData.subarray(3);\n const decrypted = this._safeStorage.decryptString(encrypted);\n const parsed = JSON.parse(decrypted);\n return parsed.data;\n }\n\n /**\n * 使用 AES-256-GCM 加密数据(后备方案)\n * @private\n */\n _encryptWithAES(data) {\n const salt = crypto.randomBytes(SALT_LENGTH);\n const iv = crypto.randomBytes(IV_LENGTH);\n const key = this._deriveKey(salt);\n const cipher = crypto.createCipheriv(ALGORITHM, key, iv);\n\n const jsonData = JSON.stringify({\n version: STORAGE_VERSION,\n data: data,\n timestamp: Date.now(),\n });\n\n const encrypted = Buffer.concat([\n cipher.update(jsonData, \"utf8\"),\n cipher.final(),\n ]);\n\n const authTag = cipher.getAuthTag();\n\n // 添加标记头以识别加密方式\n const header = Buffer.from([0x41, 0x45, STORAGE_VERSION]); // 'AE' + version\n return Buffer.concat([header, salt, iv, authTag, encrypted]);\n }\n\n /**\n * 使用 AES-256-GCM 解密数据\n * @private\n */\n _decryptWithAES(encryptedData) {\n // 跳过标记头\n const data = encryptedData.subarray(3);\n\n const salt = data.subarray(0, SALT_LENGTH);\n const iv = data.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const authTag = data.subarray(\n SALT_LENGTH + IV_LENGTH,\n SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH,\n );\n const encrypted = data.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);\n\n const key = this._deriveKey(salt);\n const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(authTag);\n\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final(),\n ]);\n\n const parsed = JSON.parse(decrypted.toString(\"utf8\"));\n return parsed.data;\n }\n\n /**\n * 判断加密方式\n * @private\n */\n _getEncryptionType(data) {\n if (data.length < 3) {\n return \"unknown\";\n }\n\n const header = data.subarray(0, 2).toString(\"ascii\");\n if (header === \"SS\") {\n return \"safeStorage\";\n }\n if (header === \"AE\") {\n return \"aes\";\n }\n\n // 旧版本没有标记头,尝试作为AES解密\n return \"legacy\";\n }\n\n /**\n * 加密数据\n * @param {Object} data - 要加密的数据对象\n * @returns {Buffer} 加密后的数据\n */\n encrypt(data) {\n if (this.safeStorageAvailable) {\n try {\n return this._encryptWithSafeStorage(data);\n } catch (error) {\n logger.warn(\n \"[SecureConfigStorage] safeStorage 加密失败,使用后备方案:\",\n error.message,\n );\n }\n }\n return this._encryptWithAES(data);\n }\n\n /**\n * 解密数据\n * @param {Buffer} encryptedData - 加密的数据\n * @returns {Object} 解密后的数据对象\n */\n decrypt(encryptedData) {\n const encType = this._getEncryptionType(encryptedData);\n\n switch (encType) {\n case \"safeStorage\":\n if (!this.safeStorageAvailable) {\n throw new Error(\"safeStorage 不可用,无法解密数据\");\n }\n return this._decryptWithSafeStorage(encryptedData);\n\n case \"aes\":\n return this._decryptWithAES(encryptedData);\n\n case \"legacy\":\n // 兼容旧版本格式\n return this._decryptLegacy(encryptedData);\n\n default:\n throw new Error(\"未知的加密格式\");\n }\n }\n\n /**\n * 解密旧版本格式\n * @private\n */\n _decryptLegacy(encryptedData) {\n // 旧版本格式: salt (32) + iv (16) + authTag (16) + encrypted\n const salt = encryptedData.subarray(0, SALT_LENGTH);\n const iv = encryptedData.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const authTag = encryptedData.subarray(\n SALT_LENGTH + IV_LENGTH,\n SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH,\n );\n const encrypted = encryptedData.subarray(\n SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH,\n );\n\n const key = this._deriveKey(salt);\n const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(authTag);\n\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final(),\n ]);\n\n return JSON.parse(decrypted.toString(\"utf8\"));\n }\n\n /**\n * 保存加密配置\n * @param {Object} config - 配置对象\n * @returns {boolean} 是否成功\n */\n save(config) {\n try {\n const dir = path.dirname(this.storagePath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n const encrypted = this.encrypt(config);\n fs.writeFileSync(this.storagePath, encrypted);\n\n // 清除缓存\n this._cache = null;\n this._cacheTimestamp = null;\n\n logger.info(\"[SecureConfigStorage] 配置已加密保存\");\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 保存失败:\", error);\n return false;\n }\n }\n\n /**\n * 加载加密配置\n * @param {boolean} useCache - 是否使用缓存\n * @returns {Object|null} 配置对象或 null\n */\n load(useCache = true) {\n try {\n // 检查缓存\n if (useCache && this._cache && this._cacheTimestamp) {\n if (Date.now() - this._cacheTimestamp < this._cacheTTL) {\n return this._cache;\n }\n }\n\n if (!fs.existsSync(this.storagePath)) {\n logger.info(\"[SecureConfigStorage] 加密配置文件不存在\");\n return null;\n }\n\n const encrypted = fs.readFileSync(this.storagePath);\n const config = this.decrypt(encrypted);\n\n // 更新缓存\n this._cache = config;\n this._cacheTimestamp = Date.now();\n\n logger.info(\"[SecureConfigStorage] 配置已解密加载\");\n return config;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 加载失败:\", error);\n return null;\n }\n }\n\n /**\n * 异步保存加密配置 (M2 启动期 IO 异步化)\n * @param {Object} config - 配置对象\n * @returns {Promise} 是否成功\n */\n async saveAsync(config) {\n try {\n const dir = path.dirname(this.storagePath);\n await fsp.mkdir(dir, { recursive: true });\n\n const encrypted = this.encrypt(config);\n await fsp.writeFile(this.storagePath, encrypted);\n\n // 清除缓存\n this._cache = null;\n this._cacheTimestamp = null;\n\n logger.info(\"[SecureConfigStorage] 配置已异步加密保存\");\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 异步保存失败:\", error);\n return false;\n }\n }\n\n /**\n * 异步加载加密配置(M2 启动期 IO 异步化)\n * @param {boolean} useCache - 是否使用缓存\n * @returns {Promise}\n */\n async loadAsync(useCache = true) {\n try {\n if (useCache && this._cache && this._cacheTimestamp) {\n if (Date.now() - this._cacheTimestamp < this._cacheTTL) {\n return this._cache;\n }\n }\n let exists = false;\n try {\n await fsp.access(this.storagePath);\n exists = true;\n } catch {\n exists = false;\n }\n if (!exists) {\n logger.info(\"[SecureConfigStorage] 加密配置文件不存在\");\n return null;\n }\n const encrypted = await fsp.readFile(this.storagePath);\n const config = this.decrypt(encrypted);\n this._cache = config;\n this._cacheTimestamp = Date.now();\n logger.info(\"[SecureConfigStorage] 配置已异步解密加载\");\n return config;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 异步加载失败:\", error);\n return null;\n }\n }\n\n /**\n * 检查是否存在加密配置\n * @returns {boolean}\n */\n exists() {\n return fs.existsSync(this.storagePath);\n }\n\n /**\n * 删除加密配置\n * @returns {boolean}\n */\n delete() {\n try {\n if (fs.existsSync(this.storagePath)) {\n fs.unlinkSync(this.storagePath);\n this._cache = null;\n this._cacheTimestamp = null;\n logger.info(\"[SecureConfigStorage] 加密配置已删除\");\n }\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 删除失败:\", error);\n return false;\n }\n }\n\n /**\n * 创建备份\n * @returns {string|null} 备份文件路径或 null\n */\n createBackup() {\n try {\n if (!this.exists()) {\n logger.info(\"[SecureConfigStorage] 没有配置可备份\");\n return null;\n }\n\n const backupDir = this._getBackupDir();\n if (!fs.existsSync(backupDir)) {\n fs.mkdirSync(backupDir, { recursive: true });\n }\n\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const backupPath = path.join(\n backupDir,\n `secure-config-${timestamp}.enc.bak`,\n );\n\n fs.copyFileSync(this.storagePath, backupPath);\n logger.info(\"[SecureConfigStorage] 备份已创建:\", backupPath);\n\n return backupPath;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 创建备份失败:\", error);\n return null;\n }\n }\n\n /**\n * 从备份恢复\n * @param {string} backupPath - 备份文件路径\n * @returns {boolean}\n */\n restoreFromBackup(backupPath) {\n try {\n if (!fs.existsSync(backupPath)) {\n logger.error(\"[SecureConfigStorage] 备份文件不存在:\", backupPath);\n return false;\n }\n\n // 先验证备份文件是否可解密\n const encrypted = fs.readFileSync(backupPath);\n this.decrypt(encrypted); // 如果解密失败会抛出异常\n\n // 备份当前文件\n if (this.exists()) {\n const currentBackup = this.storagePath + \".before-restore\";\n fs.copyFileSync(this.storagePath, currentBackup);\n }\n\n // 恢复\n fs.copyFileSync(backupPath, this.storagePath);\n this._cache = null;\n this._cacheTimestamp = null;\n\n logger.info(\"[SecureConfigStorage] 从备份恢复成功\");\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 从备份恢复失败:\", error);\n return false;\n }\n }\n\n /**\n * 列出所有备份\n * @returns {Array<{path: string, date: Date, size: number}>}\n */\n listBackups() {\n try {\n const backupDir = this._getBackupDir();\n if (!fs.existsSync(backupDir)) {\n return [];\n }\n\n const files = fs\n .readdirSync(backupDir)\n .filter((f) => f.endsWith(\".enc.bak\"))\n .map((f) => {\n const fullPath = path.join(backupDir, f);\n const stat = fs.statSync(fullPath);\n return {\n path: fullPath,\n filename: f,\n date: stat.mtime,\n size: stat.size,\n };\n })\n .sort((a, b) => b.date - a.date);\n\n return files;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 列出备份失败:\", error);\n return [];\n }\n }\n\n /**\n * 导出配置(使用密码加密)\n * @param {string} password - 导出密码\n * @param {string} exportPath - 导出文件路径\n * @returns {boolean}\n */\n exportWithPassword(password, exportPath) {\n try {\n const config = this.load(false);\n if (!config) {\n logger.error(\"[SecureConfigStorage] 没有配置可导出\");\n return false;\n }\n\n // 使用密码派生密钥\n const salt = crypto.randomBytes(SALT_LENGTH);\n const key = crypto.pbkdf2Sync(\n password,\n salt,\n ITERATIONS,\n KEY_LENGTH,\n \"sha256\",\n );\n const iv = crypto.randomBytes(IV_LENGTH);\n\n const cipher = crypto.createCipheriv(ALGORITHM, key, iv);\n const jsonData = JSON.stringify({\n version: STORAGE_VERSION,\n exportTime: new Date().toISOString(),\n data: config,\n });\n\n const encrypted = Buffer.concat([\n cipher.update(jsonData, \"utf8\"),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n\n // 格式: 'EX' + version + salt + iv + authTag + encrypted\n const header = Buffer.from([0x45, 0x58, STORAGE_VERSION]); // 'EX' + version\n const exportData = Buffer.concat([header, salt, iv, authTag, encrypted]);\n\n fs.writeFileSync(exportPath, exportData);\n logger.info(\"[SecureConfigStorage] 配置已导出:\", exportPath);\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 导出失败:\", error);\n return false;\n }\n }\n\n /**\n * 导入配置(使用密码解密)\n * @param {string} password - 导入密码\n * @param {string} importPath - 导入文件路径\n * @returns {boolean}\n */\n importWithPassword(password, importPath) {\n try {\n if (!fs.existsSync(importPath)) {\n logger.error(\"[SecureConfigStorage] 导入文件不存在\");\n return false;\n }\n\n const fileData = fs.readFileSync(importPath);\n\n // 验证格式\n if (\n fileData.length < 3 ||\n fileData.subarray(0, 2).toString(\"ascii\") !== \"EX\"\n ) {\n logger.error(\"[SecureConfigStorage] 无效的导入文件格式\");\n return false;\n }\n\n const data = fileData.subarray(3);\n const salt = data.subarray(0, SALT_LENGTH);\n const iv = data.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const authTag = data.subarray(\n SALT_LENGTH + IV_LENGTH,\n SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH,\n );\n const encrypted = data.subarray(\n SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH,\n );\n\n // 使用密码派生密钥\n const key = crypto.pbkdf2Sync(\n password,\n salt,\n ITERATIONS,\n KEY_LENGTH,\n \"sha256\",\n );\n\n const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(authTag);\n\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final(),\n ]);\n\n const parsed = JSON.parse(decrypted.toString(\"utf8\"));\n\n // 创建备份后保存\n this.createBackup();\n this.save(parsed.data);\n\n logger.info(\"[SecureConfigStorage] 配置已导入\");\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 导入失败:\", error);\n return false;\n }\n }\n\n /**\n * 获取存储信息\n * @returns {Object}\n */\n getStorageInfo() {\n const info = {\n exists: this.exists(),\n safeStorageAvailable: this.safeStorageAvailable,\n storagePath: this.storagePath,\n backupCount: 0,\n encryptionType: null,\n version: null,\n lastModified: null,\n size: null,\n };\n\n if (info.exists) {\n try {\n const stat = fs.statSync(this.storagePath);\n info.lastModified = stat.mtime;\n info.size = stat.size;\n\n const data = fs.readFileSync(this.storagePath);\n info.encryptionType = this._getEncryptionType(data);\n info.version = data.length > 2 ? data[2] : 1;\n } catch (error) {\n logger.warn(\"[SecureConfigStorage] 获取存储信息失败:\", error.message);\n }\n }\n\n info.backupCount = this.listBackups().length;\n\n return info;\n }\n\n /**\n * 迁移到更安全的加密方式\n * @returns {boolean}\n */\n migrateToSafeStorage() {\n if (!this.safeStorageAvailable) {\n logger.warn(\"[SecureConfigStorage] safeStorage 不可用,无法迁移\");\n return false;\n }\n\n try {\n const config = this.load(false);\n if (!config) {\n logger.info(\"[SecureConfigStorage] 没有配置需要迁移\");\n return true;\n }\n\n // 创建备份\n this.createBackup();\n\n // 使用 safeStorage 重新加密\n const encrypted = this._encryptWithSafeStorage(config);\n fs.writeFileSync(this.storagePath, encrypted);\n\n this._cache = null;\n this._cacheTimestamp = null;\n\n logger.info(\"[SecureConfigStorage] 已迁移到 safeStorage\");\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 迁移失败:\", error);\n return false;\n }\n }\n\n /**\n * 清除缓存\n */\n clearCache() {\n this._cache = null;\n this._cacheTimestamp = null;\n }\n}\n\n/**\n * 验证 API Key 格式\n * @param {string} provider - 提供商名称\n * @param {string} apiKey - API Key\n * @returns {{valid: boolean, message?: string}}\n */\nfunction validateApiKeyFormat(provider, apiKey) {\n if (!apiKey || typeof apiKey !== \"string\") {\n return { valid: false, message: \"API Key 不能为空\" };\n }\n\n apiKey = apiKey.trim();\n\n if (apiKey.length < 10) {\n return { valid: false, message: \"API Key 长度过短\" };\n }\n\n const pattern = API_KEY_PATTERNS[provider];\n if (pattern && !pattern.test(apiKey)) {\n return {\n valid: false,\n message: `API Key 格式不正确,应符合 ${provider} 的标准格式`,\n };\n }\n\n return { valid: true };\n}\n\n/**\n * 从配置对象中提取敏感字段\n * @param {Object} config - 完整配置\n * @returns {Object} 敏感字段\n */\nfunction extractSensitiveFields(config) {\n const sensitive = {};\n\n for (const fieldPath of SENSITIVE_FIELDS) {\n const keys = fieldPath.split(\".\");\n let value = config;\n\n for (const key of keys) {\n if (value && typeof value === \"object\" && key in value) {\n value = value[key];\n } else {\n value = undefined;\n break;\n }\n }\n\n if (value !== undefined && value !== \"\") {\n sensitive[fieldPath] = value;\n }\n }\n\n return sensitive;\n}\n\n/**\n * 将敏感字段合并回配置对象\n * @param {Object} config - 配置对象(会被修改)\n * @param {Object} sensitive - 敏感字段\n */\nfunction mergeSensitiveFields(config, sensitive) {\n for (const [fieldPath, value] of Object.entries(sensitive)) {\n const keys = fieldPath.split(\".\");\n let target = config;\n\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i];\n if (!(key in target) || typeof target[key] !== \"object\") {\n target[key] = {};\n }\n target = target[key];\n }\n\n target[keys[keys.length - 1]] = value;\n }\n}\n\n/**\n * 从配置对象中移除敏感字段(替换为占位符)\n * @param {Object} config - 配置对象\n * @returns {Object} 脱敏后的配置\n */\nfunction sanitizeConfig(config) {\n const sanitized = JSON.parse(JSON.stringify(config));\n\n for (const fieldPath of SENSITIVE_FIELDS) {\n const keys = fieldPath.split(\".\");\n let target = sanitized;\n\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i];\n if (target && typeof target === \"object\" && key in target) {\n target = target[key];\n } else {\n target = null;\n break;\n }\n }\n\n if (target && typeof target === \"object\") {\n const lastKey = keys[keys.length - 1];\n if (target[lastKey] && target[lastKey] !== \"\") {\n const original = String(target[lastKey]);\n if (original.length > 8) {\n target[lastKey] =\n original.substring(0, 4) +\n \"****\" +\n original.substring(original.length - 4);\n } else {\n target[lastKey] = \"********\";\n }\n }\n }\n }\n\n return sanitized;\n}\n\n/**\n * 检查字段是否为敏感字段\n * @param {string} fieldPath - 字段路径\n * @returns {boolean}\n */\nfunction isSensitiveField(fieldPath) {\n return SENSITIVE_FIELDS.includes(fieldPath);\n}\n\n/**\n * 获取提供商的敏感字段列表\n * @param {string} provider - 提供商名称\n * @returns {string[]}\n */\nfunction getProviderSensitiveFields(provider) {\n return SENSITIVE_FIELDS.filter((f) => f.startsWith(provider + \".\"));\n}\n\n// 单例实例\nlet instance = null;\n\n/**\n * 获取安全配置存储单例\n * @param {Object} [options] - Options forwarded to the constructor on first\n * creation only (e.g. injectable { app, safeStorage, storagePath } for tests).\n * Ignored once the singleton already exists.\n * @returns {SecureConfigStorage}\n */\nfunction getSecureConfigStorage(options = {}) {\n if (!instance) {\n instance = new SecureConfigStorage(options);\n }\n return instance;\n}\n\n/**\n * 重置单例(仅用于测试)\n */\nfunction resetInstance() {\n instance = null;\n}\n\nmodule.exports = {\n SecureConfigStorage,\n getSecureConfigStorage,\n resetInstance,\n extractSensitiveFields,\n mergeSensitiveFields,\n sanitizeConfig,\n validateApiKeyFormat,\n isSensitiveField,\n getProviderSensitiveFields,\n SENSITIVE_FIELDS,\n API_KEY_PATTERNS,\n STORAGE_VERSION,\n};\n"}, "4db48500206ff6a789c53c8197933113b06a6097": {"t": 1785809898.717189, "n": 23864, "h": "e8b0bc5ff1328f47", "c": "/**\n * 安全配置存储模块\n *\n * 提供三层加密策略:\n * 1. Electron safeStorage (操作系统凭证存储 - 最安全)\n * 2. AES-256-GCM + 机器特征派生密钥 (后备方案)\n * 3. 基于密码的加密 (用于导出/迁移)\n *\n * @module secure-config-storage\n */\n\nconst { logger } = require(\"../utils/logger.js\");\nconst crypto = require(\"crypto\");\nconst fs = require(\"fs\");\nconst fsp = require(\"fs\").promises;\nconst path = require(\"path\");\nconst { app, safeStorage } = require(\"electron\");\nconst os = require(\"os\");\n\n// 加密算法配置\nconst ALGORITHM = \"aes-256-gcm\";\nconst IV_LENGTH = 16;\nconst AUTH_TAG_LENGTH = 16;\nconst SALT_LENGTH = 32;\nconst KEY_LENGTH = 32;\nconst ITERATIONS = 100000;\n\n// 存储版本(用于未来迁移)\nconst STORAGE_VERSION = 2;\n\n/**\n * 敏感配置字段列表 - 支持14+个LLM提供商\n */\nconst SENSITIVE_FIELDS = [\n // 主要国际提供商\n \"openai.apiKey\",\n \"anthropic.apiKey\",\n \"deepseek.apiKey\",\n \"google.apiKey\", // Google AI / Gemini\n \"cohere.apiKey\", // Cohere\n \"mistral.apiKey\", // Mistral AI\n\n // 国内主要提供商\n \"volcengine.apiKey\", // 豆包(火山引擎)\n \"dashscope.apiKey\", // 阿里云通义千问\n \"zhipuai.apiKey\", // 智谱AI GLM\n \"baidu.apiKey\", // 百度千帆\n \"baidu.secretKey\", // 百度千帆 Secret Key\n \"moonshot.apiKey\", // 月之暗面 Kimi\n \"minimax.apiKey\", // MiniMax\n \"minimax.groupId\", // MiniMax Group ID\n \"xunfei.apiKey\", // 讯飞星火\n \"xunfei.apiSecret\", // 讯飞星火 Secret\n \"xunfei.appId\", // 讯飞星火 App ID\n \"stepfun.apiKey\", // 阶跃星辰\n \"yi.apiKey\", // 零一万物\n \"baichuan.apiKey\", // 百川智能\n \"sensenova.apiKey\", // 商汤日日新\n\n // 自定义和代理\n \"custom.apiKey\",\n \"custom.apiSecret\",\n \"proxy.password\", // 代理认证密码\n\n // 嵌入服务\n \"embedding.apiKey\",\n \"embedding.secretKey\",\n\n // MCP服务器凭证\n \"mcp.postgres.password\",\n \"mcp.mysql.password\",\n \"mcp.redis.password\",\n\n // 外部同步 provider 凭证(Phase 3c — WebDAV / S3 / OSS)\n // 整个 sync..* 都进 secure-config.enc 加密 blob,但只有\n // password / secret 类字段进 SENSITIVE_FIELDS(控制 sanitizeConfig\n // 渲染端 mask 行为;URL / username / remotePath 仍以明文返回给前端展示)\n \"sync.webdav.password\",\n \"sync.oss.secretAccessKey\",\n];\n\n/**\n * API Key 格式验证规则\n */\nconst API_KEY_PATTERNS = {\n openai: /^sk-[a-zA-Z0-9]{32,}$/,\n anthropic: /^sk-ant-[a-zA-Z0-9-]{32,}$/,\n deepseek: /^sk-[a-zA-Z0-9]{32,}$/,\n google: /^AIza[a-zA-Z0-9_-]{32,}$/,\n cohere: /^[a-zA-Z0-9]{32,}$/,\n volcengine: /^[a-f0-9-]{36}$/, // UUID格式\n dashscope: /^sk-[a-zA-Z0-9]{32,}$/,\n zhipuai: /^[a-zA-Z0-9]{32,}\\.[a-zA-Z0-9]+$/, // 格式: key.secret\n moonshot: /^sk-[a-zA-Z0-9]{32,}$/,\n};\n\n/**\n * 安全配置存储类\n *\n * 使用策略模式支持多种加密后端:\n * - safeStorage: Electron原生,使用操作系统凭证管理器\n * - fallback: 基于机器特征的AES-256-GCM加密\n */\nclass SecureConfigStorage {\n constructor(options = {}) {\n // Injectable Electron app / safeStorage (dependency injection) so unit tests\n // can supply mocks. vi.mock(\"electron\") does not reach this module's\n // CommonJS `require(\"electron\")` destructure in Vitest's forks pool, leaving\n // `app`/`safeStorage` undefined there. Defaults to the real Electron exports.\n this._app = options.app || app;\n this._safeStorage = options.safeStorage || safeStorage;\n this.storagePath = options.storagePath || this._getDefaultStoragePath();\n this.safeStorageAvailable = this._checkSafeStorageAvailability();\n this.machineKey = null;\n this._cache = null;\n this._cacheTimestamp = null;\n this._cacheTTL = 5 * 60 * 1000; // 5分钟缓存\n }\n\n /**\n * 检查 Electron safeStorage 是否可用\n * @private\n */\n _checkSafeStorageAvailability() {\n try {\n if (\n this._safeStorage &&\n typeof this._safeStorage.isEncryptionAvailable === \"function\"\n ) {\n const available = this._safeStorage.isEncryptionAvailable();\n logger.info(`[SecureConfigStorage] safeStorage 可用性: ${available}`);\n return available;\n }\n } catch (error) {\n logger.warn(\"[SecureConfigStorage] safeStorage 检查失败:\", error.message);\n }\n return false;\n }\n\n /**\n * 获取默认存储路径\n * @private\n */\n _getDefaultStoragePath() {\n const userDataPath = this._app.getPath(\"userData\");\n return path.join(userDataPath, \"secure-config.enc\");\n }\n\n /**\n * 获取备份目录路径\n * @private\n */\n _getBackupDir() {\n const userDataPath = this._app.getPath(\"userData\");\n return path.join(userDataPath, \"secure-backups\");\n }\n\n /**\n * 获取机器特定的密钥种子\n * @private\n */\n _getMachineKeySeed() {\n const components = [\n os.hostname(),\n os.platform(),\n os.arch(),\n os.homedir(),\n JSON.stringify(\n os\n .cpus()\n .map((c) => c.model)\n .slice(0, 1),\n ),\n \"chainlesschain-llm-config-v2\",\n ];\n return components.join(\"|\");\n }\n\n /**\n * 从机器种子派生加密密钥\n * @private\n */\n _deriveKey(salt) {\n const seed = this._getMachineKeySeed();\n return crypto.pbkdf2Sync(seed, salt, ITERATIONS, KEY_LENGTH, \"sha256\");\n }\n\n /**\n * 使用 safeStorage 加密数据\n * @private\n */\n _encryptWithSafeStorage(data) {\n const jsonData = JSON.stringify({\n version: STORAGE_VERSION,\n data: data,\n timestamp: Date.now(),\n });\n const encrypted = this._safeStorage.encryptString(jsonData);\n\n // 添加标记头以识别加密方式\n const header = Buffer.from([0x53, 0x53, STORAGE_VERSION]); // 'SS' + version\n return Buffer.concat([header, encrypted]);\n }\n\n /**\n * 使用 safeStorage 解密数据\n * @private\n */\n _decryptWithSafeStorage(encryptedData) {\n // 跳过标记头\n const encrypted = encryptedData.subarray(3);\n const decrypted = this._safeStorage.decryptString(encrypted);\n const parsed = JSON.parse(decrypted);\n return parsed.data;\n }\n\n /**\n * 使用 AES-256-GCM 加密数据(后备方案)\n * @private\n */\n _encryptWithAES(data) {\n const salt = crypto.randomBytes(SALT_LENGTH);\n const iv = crypto.randomBytes(IV_LENGTH);\n const key = this._deriveKey(salt);\n const cipher = crypto.createCipheriv(ALGORITHM, key, iv);\n\n const jsonData = JSON.stringify({\n version: STORAGE_VERSION,\n data: data,\n timestamp: Date.now(),\n });\n\n const encrypted = Buffer.concat([\n cipher.update(jsonData, \"utf8\"),\n cipher.final(),\n ]);\n\n const authTag = cipher.getAuthTag();\n\n // 添加标记头以识别加密方式\n const header = Buffer.from([0x41, 0x45, STORAGE_VERSION]); // 'AE' + version\n return Buffer.concat([header, salt, iv, authTag, encrypted]);\n }\n\n /**\n * 使用 AES-256-GCM 解密数据\n * @private\n */\n _decryptWithAES(encryptedData) {\n // 跳过标记头\n const data = encryptedData.subarray(3);\n\n const salt = data.subarray(0, SALT_LENGTH);\n const iv = data.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const authTag = data.subarray(\n SALT_LENGTH + IV_LENGTH,\n SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH,\n );\n const encrypted = data.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);\n\n const key = this._deriveKey(salt);\n const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(authTag);\n\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final(),\n ]);\n\n const parsed = JSON.parse(decrypted.toString(\"utf8\"));\n return parsed.data;\n }\n\n /**\n * 判断加密方式\n * @private\n */\n _getEncryptionType(data) {\n if (data.length < 3) {\n return \"unknown\";\n }\n\n const header = data.subarray(0, 2).toString(\"ascii\");\n if (header === \"SS\") {\n return \"safeStorage\";\n }\n if (header === \"AE\") {\n return \"aes\";\n }\n\n // 旧版本没有标记头,尝试作为AES解密\n return \"legacy\";\n }\n\n /**\n * 加密数据\n * @param {Object} data - 要加密的数据对象\n * @returns {Buffer} 加密后的数据\n */\n encrypt(data) {\n if (this.safeStorageAvailable) {\n try {\n return this._encryptWithSafeStorage(data);\n } catch (error) {\n logger.warn(\n \"[SecureConfigStorage] safeStorage 加密失败,使用后备方案:\",\n error.message,\n );\n }\n }\n return this._encryptWithAES(data);\n }\n\n /**\n * 解密数据\n * @param {Buffer} encryptedData - 加密的数据\n * @returns {Object} 解密后的数据对象\n */\n decrypt(encryptedData) {\n const encType = this._getEncryptionType(encryptedData);\n\n switch (encType) {\n case \"safeStorage\":\n if (!this.safeStorageAvailable) {\n throw new Error(\"safeStorage 不可用,无法解密数据\");\n }\n return this._decryptWithSafeStorage(encryptedData);\n\n case \"aes\":\n return this._decryptWithAES(encryptedData);\n\n case \"legacy\":\n // 兼容旧版本格式\n return this._decryptLegacy(encryptedData);\n\n default:\n throw new Error(\"未知的加密格式\");\n }\n }\n\n /**\n * 解密旧版本格式\n * @private\n */\n _decryptLegacy(encryptedData) {\n // 旧版本格式: salt (32) + iv (16) + authTag (16) + encrypted\n const salt = encryptedData.subarray(0, SALT_LENGTH);\n const iv = encryptedData.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const authTag = encryptedData.subarray(\n SALT_LENGTH + IV_LENGTH,\n SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH,\n );\n const encrypted = encryptedData.subarray(\n SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH,\n );\n\n const key = this._deriveKey(salt);\n const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(authTag);\n\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final(),\n ]);\n\n return JSON.parse(decrypted.toString(\"utf8\"));\n }\n\n /**\n * 保存加密配置\n * @param {Object} config - 配置对象\n * @returns {boolean} 是否成功\n */\n save(config) {\n try {\n const dir = path.dirname(this.storagePath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n const encrypted = this.encrypt(config);\n fs.writeFileSync(this.storagePath, encrypted);\n\n // 清除缓存\n this._cache = null;\n this._cacheTimestamp = null;\n\n logger.info(\"[SecureConfigStorage] 配置已加密保存\");\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 保存失败:\", error);\n return false;\n }\n }\n\n /**\n * 加载加密配置\n * @param {boolean} useCache - 是否使用缓存\n * @returns {Object|null} 配置对象或 null\n */\n load(useCache = true) {\n try {\n // 检查缓存\n if (useCache && this._cache && this._cacheTimestamp) {\n if (Date.now() - this._cacheTimestamp < this._cacheTTL) {\n return this._cache;\n }\n }\n\n if (!fs.existsSync(this.storagePath)) {\n logger.info(\"[SecureConfigStorage] 加密配置文件不存在\");\n return null;\n }\n\n const encrypted = fs.readFileSync(this.storagePath);\n const config = this.decrypt(encrypted);\n\n // 更新缓存\n this._cache = config;\n this._cacheTimestamp = Date.now();\n\n logger.info(\"[SecureConfigStorage] 配置已解密加载\");\n return config;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 加载失败:\", error);\n return null;\n }\n }\n\n /**\n * 异步保存加密配置 (M2 启动期 IO 异步化)\n * @param {Object} config - 配置对象\n * @returns {Promise} 是否成功\n */\n async saveAsync(config) {\n try {\n const dir = path.dirname(this.storagePath);\n await fsp.mkdir(dir, { recursive: true });\n\n const encrypted = this.encrypt(config);\n await fsp.writeFile(this.storagePath, encrypted);\n\n // 清除缓存\n this._cache = null;\n this._cacheTimestamp = null;\n\n logger.info(\"[SecureConfigStorage] 配置已异步加密保存\");\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 异步保存失败:\", error);\n return false;\n }\n }\n\n /**\n * 异步加载加密配置(M2 启动期 IO 异步化)\n * @param {boolean} useCache - 是否使用缓存\n * @returns {Promise}\n */\n async loadAsync(useCache = true) {\n try {\n if (useCache && this._cache && this._cacheTimestamp) {\n if (Date.now() - this._cacheTimestamp < this._cacheTTL) {\n return this._cache;\n }\n }\n let exists = false;\n try {\n await fsp.access(this.storagePath);\n exists = true;\n } catch {\n exists = false;\n }\n if (!exists) {\n logger.info(\"[SecureConfigStorage] 加密配置文件不存在\");\n return null;\n }\n const encrypted = await fsp.readFile(this.storagePath);\n const config = this.decrypt(encrypted);\n this._cache = config;\n this._cacheTimestamp = Date.now();\n logger.info(\"[SecureConfigStorage] 配置已异步解密加载\");\n return config;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 异步加载失败:\", error);\n return null;\n }\n }\n\n /**\n * 检查是否存在加密配置\n * @returns {boolean}\n */\n exists() {\n return fs.existsSync(this.storagePath);\n }\n\n /**\n * 删除加密配置\n * @returns {boolean}\n */\n delete() {\n try {\n if (fs.existsSync(this.storagePath)) {\n fs.unlinkSync(this.storagePath);\n this._cache = null;\n this._cacheTimestamp = null;\n logger.info(\"[SecureConfigStorage] 加密配置已删除\");\n }\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 删除失败:\", error);\n return false;\n }\n }\n\n /**\n * 创建备份\n * @returns {string|null} 备份文件路径或 null\n */\n createBackup() {\n try {\n if (!this.exists()) {\n logger.info(\"[SecureConfigStorage] 没有配置可备份\");\n return null;\n }\n\n const backupDir = this._getBackupDir();\n if (!fs.existsSync(backupDir)) {\n fs.mkdirSync(backupDir, { recursive: true });\n }\n\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const backupPath = path.join(\n backupDir,\n `secure-config-${timestamp}.enc.bak`,\n );\n\n fs.copyFileSync(this.storagePath, backupPath);\n logger.info(\"[SecureConfigStorage] 备份已创建:\", backupPath);\n\n return backupPath;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 创建备份失败:\", error);\n return null;\n }\n }\n\n /**\n * 从备份恢复\n * @param {string} backupPath - 备份文件路径\n * @returns {boolean}\n */\n restoreFromBackup(backupPath) {\n try {\n if (!fs.existsSync(backupPath)) {\n logger.error(\"[SecureConfigStorage] 备份文件不存在:\", backupPath);\n return false;\n }\n\n // 先验证备份文件是否可解密\n const encrypted = fs.readFileSync(backupPath);\n this.decrypt(encrypted); // 如果解密失败会抛出异常\n\n // 备份当前文件\n if (this.exists()) {\n const currentBackup = this.storagePath + \".before-restore\";\n fs.copyFileSync(this.storagePath, currentBackup);\n }\n\n // 恢复\n fs.copyFileSync(backupPath, this.storagePath);\n this._cache = null;\n this._cacheTimestamp = null;\n\n logger.info(\"[SecureConfigStorage] 从备份恢复成功\");\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 从备份恢复失败:\", error);\n return false;\n }\n }\n\n /**\n * 列出所有备份\n * @returns {Array<{path: string, date: Date, size: number}>}\n */\n listBackups() {\n try {\n const backupDir = this._getBackupDir();\n if (!fs.existsSync(backupDir)) {\n return [];\n }\n\n const files = fs\n .readdirSync(backupDir)\n .filter((f) => f.endsWith(\".enc.bak\"))\n .map((f) => {\n const fullPath = path.join(backupDir, f);\n const stat = fs.statSync(fullPath);\n return {\n path: fullPath,\n filename: f,\n date: stat.mtime,\n size: stat.size,\n };\n })\n .sort((a, b) => b.date - a.date);\n\n return files;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 列出备份失败:\", error);\n return [];\n }\n }\n\n /**\n * 导出配置(使用密码加密)\n * @param {string} password - 导出密码\n * @param {string} exportPath - 导出文件路径\n * @returns {boolean}\n */\n exportWithPassword(password, exportPath) {\n try {\n const config = this.load(false);\n if (!config) {\n logger.error(\"[SecureConfigStorage] 没有配置可导出\");\n return false;\n }\n\n // 使用密码派生密钥\n const salt = crypto.randomBytes(SALT_LENGTH);\n const key = crypto.pbkdf2Sync(\n password,\n salt,\n ITERATIONS,\n KEY_LENGTH,\n \"sha256\",\n );\n const iv = crypto.randomBytes(IV_LENGTH);\n\n const cipher = crypto.createCipheriv(ALGORITHM, key, iv);\n const jsonData = JSON.stringify({\n version: STORAGE_VERSION,\n exportTime: new Date().toISOString(),\n data: config,\n });\n\n const encrypted = Buffer.concat([\n cipher.update(jsonData, \"utf8\"),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n\n // 格式: 'EX' + version + salt + iv + authTag + encrypted\n const header = Buffer.from([0x45, 0x58, STORAGE_VERSION]); // 'EX' + version\n const exportData = Buffer.concat([header, salt, iv, authTag, encrypted]);\n\n fs.writeFileSync(exportPath, exportData);\n logger.info(\"[SecureConfigStorage] 配置已导出:\", exportPath);\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 导出失败:\", error);\n return false;\n }\n }\n\n /**\n * 导入配置(使用密码解密)\n * @param {string} password - 导入密码\n * @param {string} importPath - 导入文件路径\n * @returns {boolean}\n */\n importWithPassword(password, importPath) {\n try {\n if (!fs.existsSync(importPath)) {\n logger.error(\"[SecureConfigStorage] 导入文件不存在\");\n return false;\n }\n\n const fileData = fs.readFileSync(importPath);\n\n // 验证格式\n if (\n fileData.length < 3 ||\n fileData.subarray(0, 2).toString(\"ascii\") !== \"EX\"\n ) {\n logger.error(\"[SecureConfigStorage] 无效的导入文件格式\");\n return false;\n }\n\n const data = fileData.subarray(3);\n const salt = data.subarray(0, SALT_LENGTH);\n const iv = data.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const authTag = data.subarray(\n SALT_LENGTH + IV_LENGTH,\n SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH,\n );\n const encrypted = data.subarray(\n SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH,\n );\n\n // 使用密码派生密钥\n const key = crypto.pbkdf2Sync(\n password,\n salt,\n ITERATIONS,\n KEY_LENGTH,\n \"sha256\",\n );\n\n const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(authTag);\n\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final(),\n ]);\n\n const parsed = JSON.parse(decrypted.toString(\"utf8\"));\n\n // 创建备份后保存\n this.createBackup();\n this.save(parsed.data);\n\n logger.info(\"[SecureConfigStorage] 配置已导入\");\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 导入失败:\", error);\n return false;\n }\n }\n\n /**\n * 获取存储信息\n * @returns {Object}\n */\n getStorageInfo() {\n const info = {\n exists: this.exists(),\n safeStorageAvailable: this.safeStorageAvailable,\n storagePath: this.storagePath,\n backupCount: 0,\n encryptionType: null,\n version: null,\n lastModified: null,\n size: null,\n };\n\n if (info.exists) {\n try {\n const stat = fs.statSync(this.storagePath);\n info.lastModified = stat.mtime;\n info.size = stat.size;\n\n const data = fs.readFileSync(this.storagePath);\n info.encryptionType = this._getEncryptionType(data);\n info.version = data.length > 2 ? data[2] : 1;\n } catch (error) {\n logger.warn(\"[SecureConfigStorage] 获取存储信息失败:\", error.message);\n }\n }\n\n info.backupCount = this.listBackups().length;\n\n return info;\n }\n\n /**\n * 迁移到更安全的加密方式\n * @returns {boolean}\n */\n migrateToSafeStorage() {\n if (!this.safeStorageAvailable) {\n logger.warn(\"[SecureConfigStorage] safeStorage 不可用,无法迁移\");\n return false;\n }\n\n try {\n const config = this.load(false);\n if (!config) {\n logger.info(\"[SecureConfigStorage] 没有配置需要迁移\");\n return true;\n }\n\n // 创建备份\n this.createBackup();\n\n // 使用 safeStorage 重新加密\n const encrypted = this._encryptWithSafeStorage(config);\n fs.writeFileSync(this.storagePath, encrypted);\n\n this._cache = null;\n this._cacheTimestamp = null;\n\n logger.info(\"[SecureConfigStorage] 已迁移到 safeStorage\");\n return true;\n } catch (error) {\n logger.error(\"[SecureConfigStorage] 迁移失败:\", error);\n return false;\n }\n }\n\n /**\n * 清除缓存\n */\n clearCache() {\n this._cache = null;\n this._cacheTimestamp = null;\n }\n}\n\n/**\n * 验证 API Key 格式\n * @param {string} provider - 提供商名称\n * @param {string} apiKey - API Key\n * @returns {{valid: boolean, message?: string}}\n */\nfunction validateApiKeyFormat(provider, apiKey) {\n if (!apiKey || typeof apiKey !== \"string\") {\n return { valid: false, message: \"API Key 不能为空\" };\n }\n\n apiKey = apiKey.trim();\n\n if (apiKey.length < 10) {\n return { valid: false, message: \"API Key 长度过短\" };\n }\n\n const pattern = API_KEY_PATTERNS[provider];\n if (pattern && !pattern.test(apiKey)) {\n return {\n valid: false,\n message: `API Key 格式不正确,应符合 ${provider} 的标准格式`,\n };\n }\n\n return { valid: true };\n}\n\n/**\n * 从配置对象中提取敏感字段\n * @param {Object} config - 完整配置\n * @returns {Object} 敏感字段\n */\nfunction extractSensitiveFields(config) {\n const sensitive = {};\n\n for (const fieldPath of SENSITIVE_FIELDS) {\n const keys = fieldPath.split(\".\");\n let value = config;\n\n for (const key of keys) {\n if (value && typeof value === \"object\" && key in value) {\n value = value[key];\n } else {\n value = undefined;\n break;\n }\n }\n\n if (value !== undefined && value !== \"\") {\n sensitive[fieldPath] = value;\n }\n }\n\n return sensitive;\n}\n\n/**\n * 将敏感字段合并回配置对象\n * @param {Object} config - 配置对象(会被修改)\n * @param {Object} sensitive - 敏感字段\n */\nfunction mergeSensitiveFields(config, sensitive) {\n for (const [fieldPath, value] of Object.entries(sensitive)) {\n const keys = fieldPath.split(\".\");\n let target = config;\n\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i];\n if (!(key in target) || typeof target[key] !== \"object\") {\n target[key] = {};\n }\n target = target[key];\n }\n\n target[keys[keys.length - 1]] = value;\n }\n}\n\n/**\n * 从配置对象中移除敏感字段(替换为占位符)\n * @param {Object} config - 配置对象\n * @returns {Object} 脱敏后的配置\n */\nfunction sanitizeConfig(config) {\n const sanitized = JSON.parse(JSON.stringify(config));\n\n for (const fieldPath of SENSITIVE_FIELDS) {\n const keys = fieldPath.split(\".\");\n let target = sanitized;\n\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i];\n if (target && typeof target === \"object\" && key in target) {\n target = target[key];\n } else {\n target = null;\n break;\n }\n }\n\n if (target && typeof target === \"object\") {\n const lastKey = keys[keys.length - 1];\n if (target[lastKey] && target[lastKey] !== \"\") {\n const original = String(target[lastKey]);\n if (original.length > 8) {\n target[lastKey] =\n original.substring(0, 4) +\n \"****\" +\n original.substring(original.length - 4);\n } else {\n target[lastKey] = \"********\";\n }\n }\n }\n }\n\n return sanitized;\n}\n\n/**\n * 检查字段是否为敏感字段\n * @param {string} fieldPath - 字段路径\n * @returns {boolean}\n */\nfunction isSensitiveField(fieldPath) {\n return SENSITIVE_FIELDS.includes(fieldPath);\n}\n\n/**\n * 获取提供商的敏感字段列表\n * @param {string} provider - 提供商名称\n * @returns {string[]}\n */\nfunction getProviderSensitiveFields(provider) {\n return SENSITIVE_FIELDS.filter((f) => f.startsWith(provider + \".\"));\n}\n\n// 单例实例\nlet instance = null;\n\n/**\n * 获取安全配置存储单例\n * @param {Object} [options] - Options forwarded to the constructor on first\n * creation only (e.g. injectable { app, safeStorage, storagePath } for tests).\n * Ignored once the singleton already exists.\n * @returns {SecureConfigStorage}\n */\nfunction getSecureConfigStorage(options = {}) {\n if (!instance) {\n instance = new SecureConfigStorage(options);\n }\n return instance;\n}\n\n/**\n * 重置单例(仅用于测试)\n */\nfunction resetInstance() {\n instance = null;\n}\n\nmodule.exports = {\n SecureConfigStorage,\n getSecureConfigStorage,\n resetInstance,\n extractSensitiveFields,\n mergeSensitiveFields,\n sanitizeConfig,\n validateApiKeyFormat,\n isSensitiveField,\n getProviderSensitiveFields,\n SENSITIVE_FIELDS,\n API_KEY_PATTERNS,\n STORAGE_VERSION,\n};\n"}}, "docs-site/docs/chainlesschain/cli-chat.md": {"e8e7ba274b487ed491c04ec3359841a0e545debb": {"t": 1785809875.9408877, "n": 6212, "h": "72e4d9088a5ad105", "c": "# AI 对话 (chat/ask)\n\n> Headless 命令 — 不依赖桌面 GUI,直接使用核心包运行。适用于服务器、CI/CD、容器化等无桌面环境。\n\n## 概述\n\n`chat` 和 `ask` 命令提供命令行环境下的 AI 对话能力,支持 8 个 LLM 提供商的流式交互式对话和单次问答。`chat` 支持代理模式(`--agent`)以启用文件读写和命令执行等工具调用,对话历史自动保存到本地数据库并追踪 Token 用量。\n\n## 核心特性\n\n- 🗣️ **流式输出**: 实时显示 AI 回复,支持 Markdown 渲染\n- 🔄 **多提供商**: 支持 Ollama、OpenAI、Anthropic、DeepSeek、DashScope、Gemini、Mistral、Volcengine(豆包) 等 8 个提供商\n- 🤖 **代理模式**: `--agent` 参数启用工具调用(读写文件、执行命令)\n- 📝 **会话自动保存**: 对话历史自动持久化到数据库\n- 💰 **Token 追踪**: 自动记录每次对话的 Token 用量和成本\n\n## 系统架构\n\n```\nchat 命令 → chat-repl.js (REPL 循环)\n │\n ┌───────────┼───────────┐\n ▼ ▼ ▼\n 用户输入 llm-providers session-manager\n (readline) (8 providers) (自动保存)\n │ │ │\n ▼ ▼ ▼\n 斜杠命令 流式 API 调用 SQLite sessions 表\n 解析 token-tracker\n```\n\n## chat — 交互式对话\n\n启动交互式 AI 对话,支持流式输出。\n\n```bash\nchainlesschain chat # 默认: Ollama qwen2:7b\nchainlesschain chat --model llama3 # 使用其他模型\nchainlesschain chat --provider openai --api-key sk-...\nchainlesschain chat --agent # 代理模式(可读写文件、执行命令)\nchainlesschain chat --agent --session # 代理模式 + 恢复历史会话\n```\n\n### 对话内斜杠命令\n\n| 命令 | 说明 |\n| ----------- | ------------ |\n| `/exit` | 退出对话 |\n| `/model` | 切换模型 |\n| `/provider` | 切换提供商 |\n| `/clear` | 清空对话历史 |\n| `/history` | 查看对话历史 |\n| `/help` | 显示帮助 |\n\n## ask — 单次问答\n\n非交互式单次 AI 问答,适用于脚本和 CI/CD 流水线。\n\n```bash\nchainlesschain ask \"什么是WebRTC?\"\nchainlesschain ask \"解释这段代码\" --model gpt-4o --provider openai\nchainlesschain ask \"Hello\" --json # JSON输出(含问题/回答/模型信息)\n```\n\n## 配置参考\n\n```bash\nchainlesschain chat [options] # 交互式对话\nchainlesschain ask [options] # 单次问答\n\n选项:\n --model 模型名称(默认 qwen2:7b)\n --provider LLM 提供商 (ollama/openai/volcengine/deepseek/\n dashscope/gemini/mistral/anthropic)\n --base-url API 基础地址(中转站/代理)\n --api-key API 密钥(可从环境变量读取)\n --agent 启用代理模式(工具调用)\n --session 恢复历史会话(配合 --agent)\n --json JSON 输出(ask 专用)\n\n对话内斜杠命令(chat REPL):\n /exit /model /provider /clear /history /help\n```\n\n会话历史自动保存在 SQLite `sessions` 表中。Token 用量由 token-tracker 自动记录。\n\n## 性能指标\n\n| 操作 | 目标 | 实际 | 状态 |\n| -------------------------------- | ------- | ------ | ---- |\n| REPL 启动 | < 200ms | ~80ms | ✅ |\n| 流式首 token 延迟(Ollama 本地) | < 1s | ~400ms | ✅ |\n| 流式首 token 延迟(云端 API) | < 2s | ~800ms | ✅ |\n| 斜杠命令解析 | < 20ms | ~5ms | ✅ |\n| 会话持久化(每轮) | < 50ms | ~15ms | ✅ |\n| ask 单次问答端到端 | < 5s | ~2s | ✅ |\n\n## 测试覆盖率\n\n```\n✅ chat.test.js - 覆盖 chat/ask CLI 的主要路径\n ├── 参数解析 / 选项验证(provider/model/base-url/agent/session)\n ├── 正常路径(流式输出、8 provider 适配、agent 模式、ask 单次)\n ├── 错误处理 / 边界情况(连接失败、401、会话不存在、中文编码)\n └── JSON 输出格式(ask --json)\n```\n\n## 关键文件\n\n- `packages/cli/src/commands/chat.js` — chat/ask 命令实现\n- `packages/cli/src/repl/chat-repl.js` — 交互式对话 REPL(流式输出)\n- `packages/cli/src/lib/llm-providers.js` — 多 LLM 提供商适配\n\n## 安全考虑\n\n- API Key 通过 `--api-key` 传递时不会写入历史记录\n- 对话内容仅保存在本地数据库,不上传到第三方服务\n- 使用 `--provider ollama` 时数据完全离线处理\n\n## 使用示例\n\n### 场景 1:本地离线对话\n\n```bash\nchainlesschain chat\nchainlesschain chat --model llama3\n```\n\n使用本地 Ollama 模型进行交互式对话,数据完全不出设备,适合处理敏感信息。\n\n### 场景 2:使用火山引擎(豆包)聊天\n\n```bash\n# 设置环境变量\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\n\n# 使用旗舰模型\nchainlesschain chat --provider volcengine\n\n# 使用快速模型(成本更低、响应更快)\nchainlesschain chat --provider volcengine --model doubao-seed-1-6-flash-250828\n\n# 使用代码模型\nchainlesschain chat --provider volcengine --model doubao-seed-code\n```\n\n### 场景 3:使用 OpenAI / DeepSeek 聊天\n\n```bash\n# OpenAI\nexport OPENAI_API_KEY=sk-xxxxxxxxxxxx\nchainlesschain chat --provider openai --model gpt-4o\n\n# DeepSeek\nexport DEEPSEEK_API_KEY=sk-xxxxxxxxxxxx\nchainlesschain chat --provider deepseek --model deepseek-chat\n```\n\n### 场景 4:通过中转站聊天(无需科学上网)\n\n```bash\n# 通过 API2D 中转站使用 GPT-4o\nchainlesschain chat \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n\n# 通过 One-API 网关使用任意模型\nchainlesschain chat \\\n --provider openai \\\n --base-url http://localhost:3000/v1 \\\n --api-key sk-oneapi-xxxxxx \\\n --model deepseek-chat\n\n# 火山引擎通过代理访问\nchainlesschain chat \\\n --provider volcengine \\\n --base-url https://your-proxy.com/volcengine/v3 \\\n --model doubao-seed-1-6-251015\n```\n\n### 场景 5:Agent 模式辅助开发\n\n```bash\n# 本地 Ollama\nchainlesschain chat --agent\n\n# 火山引擎 Agent 模式(自动智能选择模型)\nchainlesschain chat --agent --provider volcengine\n\n# 通过中转站使用 GPT-4o Agent 模式\nchainlesschain chat --agent \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n```\n\n启用代理模式后,AI 可以读写文件、执行命令,适合代码生成、文件修改等开发场景。对话中使用 `/help` 查看可用工具。\n\n### 场景 6:企业自建代理聊天\n\n```bash\n# Nginx 反向代理\nchainlesschain chat \\\n --provider openai \\\n --base-url https://ai-gateway.company.com/v1 \\\n --model gpt-4o\n\n# vLLM 部署的本地模型\nchainlesschain chat \\\n --provider openai \\\n --base-url http://localhost:8000/v1 \\\n --api-key dummy \\\n --model my-finetuned-llama\n```\n\n### 场景 7:脚本集成单次问答 (ask)\n\n```bash\nchainlesschain ask \"将以下 JSON 转为 YAML 格式:{\\\"name\\\":\\\"test\\\"}\" --json\nchainlesschain ask \"解释 WebRTC 的 ICE 候选流程\" --provider openai --api-key sk-xxx\nchainlesschain ask \"什么是机器学习?\" --provider volcengine\n```\n\n非交互模式适合在 CI/CD 流水线或脚本中调用,`--json` 输出便于程序解析。\n\n## 故障排查\n\n| 问题 | 解决方案 |\n| ------------------- | --------------------------------------------------- |\n| 连接 Ollama 超时 | 确认 Ollama 已启动:`ollama serve`,检查端口 11434 |\n| OpenAI API 报错 401 | 检查 API Key 是否正确,是否有余额 |\n| 中转站返回 401 | 检查 `--api-key` 格式,中转站密钥格式可能与官方不同 |\n| 火山引擎认证失败 | 确认 `VOLCENGINE_API_KEY` 以 `ark-` 开头 |\n| 中文乱码 | 确保终端编码为 UTF-8:`chcp 65001` (Windows) |\n| 流式输出卡住 | 检查网络连接,或切换到本地模型 |\n| `--base-url` 不生效 | 确保 URL 包含 `/v1` 后缀 |\n\n## 相关文档\n\n- [LLM 管理](./cli-llm) — 模型列表与提供商管理\n- [LLM 中转站与自定义接入](./cli-llm-proxy) — 中转站、代理、自建网关完整指南\n- [代理模式](./cli-agent) — 带工具调用的高级对话\n- [会话管理](./cli-session) — 对话历史持久化与恢复\n- [Token 追踪](./cli-tokens) — 用量统计与成本分析\n"}, "4db48500206ff6a789c53c8197933113b06a6097": {"t": 1785809910.102505, "n": 6212, "h": "72e4d9088a5ad105", "c": "# AI 对话 (chat/ask)\n\n> Headless 命令 — 不依赖桌面 GUI,直接使用核心包运行。适用于服务器、CI/CD、容器化等无桌面环境。\n\n## 概述\n\n`chat` 和 `ask` 命令提供命令行环境下的 AI 对话能力,支持 8 个 LLM 提供商的流式交互式对话和单次问答。`chat` 支持代理模式(`--agent`)以启用文件读写和命令执行等工具调用,对话历史自动保存到本地数据库并追踪 Token 用量。\n\n## 核心特性\n\n- 🗣️ **流式输出**: 实时显示 AI 回复,支持 Markdown 渲染\n- 🔄 **多提供商**: 支持 Ollama、OpenAI、Anthropic、DeepSeek、DashScope、Gemini、Mistral、Volcengine(豆包) 等 8 个提供商\n- 🤖 **代理模式**: `--agent` 参数启用工具调用(读写文件、执行命令)\n- 📝 **会话自动保存**: 对话历史自动持久化到数据库\n- 💰 **Token 追踪**: 自动记录每次对话的 Token 用量和成本\n\n## 系统架构\n\n```\nchat 命令 → chat-repl.js (REPL 循环)\n │\n ┌───────────┼───────────┐\n ▼ ▼ ▼\n 用户输入 llm-providers session-manager\n (readline) (8 providers) (自动保存)\n │ │ │\n ▼ ▼ ▼\n 斜杠命令 流式 API 调用 SQLite sessions 表\n 解析 token-tracker\n```\n\n## chat — 交互式对话\n\n启动交互式 AI 对话,支持流式输出。\n\n```bash\nchainlesschain chat # 默认: Ollama qwen2:7b\nchainlesschain chat --model llama3 # 使用其他模型\nchainlesschain chat --provider openai --api-key sk-...\nchainlesschain chat --agent # 代理模式(可读写文件、执行命令)\nchainlesschain chat --agent --session # 代理模式 + 恢复历史会话\n```\n\n### 对话内斜杠命令\n\n| 命令 | 说明 |\n| ----------- | ------------ |\n| `/exit` | 退出对话 |\n| `/model` | 切换模型 |\n| `/provider` | 切换提供商 |\n| `/clear` | 清空对话历史 |\n| `/history` | 查看对话历史 |\n| `/help` | 显示帮助 |\n\n## ask — 单次问答\n\n非交互式单次 AI 问答,适用于脚本和 CI/CD 流水线。\n\n```bash\nchainlesschain ask \"什么是WebRTC?\"\nchainlesschain ask \"解释这段代码\" --model gpt-4o --provider openai\nchainlesschain ask \"Hello\" --json # JSON输出(含问题/回答/模型信息)\n```\n\n## 配置参考\n\n```bash\nchainlesschain chat [options] # 交互式对话\nchainlesschain ask [options] # 单次问答\n\n选项:\n --model 模型名称(默认 qwen2:7b)\n --provider LLM 提供商 (ollama/openai/volcengine/deepseek/\n dashscope/gemini/mistral/anthropic)\n --base-url API 基础地址(中转站/代理)\n --api-key API 密钥(可从环境变量读取)\n --agent 启用代理模式(工具调用)\n --session 恢复历史会话(配合 --agent)\n --json JSON 输出(ask 专用)\n\n对话内斜杠命令(chat REPL):\n /exit /model /provider /clear /history /help\n```\n\n会话历史自动保存在 SQLite `sessions` 表中。Token 用量由 token-tracker 自动记录。\n\n## 性能指标\n\n| 操作 | 目标 | 实际 | 状态 |\n| -------------------------------- | ------- | ------ | ---- |\n| REPL 启动 | < 200ms | ~80ms | ✅ |\n| 流式首 token 延迟(Ollama 本地) | < 1s | ~400ms | ✅ |\n| 流式首 token 延迟(云端 API) | < 2s | ~800ms | ✅ |\n| 斜杠命令解析 | < 20ms | ~5ms | ✅ |\n| 会话持久化(每轮) | < 50ms | ~15ms | ✅ |\n| ask 单次问答端到端 | < 5s | ~2s | ✅ |\n\n## 测试覆盖率\n\n```\n✅ chat.test.js - 覆盖 chat/ask CLI 的主要路径\n ├── 参数解析 / 选项验证(provider/model/base-url/agent/session)\n ├── 正常路径(流式输出、8 provider 适配、agent 模式、ask 单次)\n ├── 错误处理 / 边界情况(连接失败、401、会话不存在、中文编码)\n └── JSON 输出格式(ask --json)\n```\n\n## 关键文件\n\n- `packages/cli/src/commands/chat.js` — chat/ask 命令实现\n- `packages/cli/src/repl/chat-repl.js` — 交互式对话 REPL(流式输出)\n- `packages/cli/src/lib/llm-providers.js` — 多 LLM 提供商适配\n\n## 安全考虑\n\n- API Key 通过 `--api-key` 传递时不会写入历史记录\n- 对话内容仅保存在本地数据库,不上传到第三方服务\n- 使用 `--provider ollama` 时数据完全离线处理\n\n## 使用示例\n\n### 场景 1:本地离线对话\n\n```bash\nchainlesschain chat\nchainlesschain chat --model llama3\n```\n\n使用本地 Ollama 模型进行交互式对话,数据完全不出设备,适合处理敏感信息。\n\n### 场景 2:使用火山引擎(豆包)聊天\n\n```bash\n# 设置环境变量\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\n\n# 使用旗舰模型\nchainlesschain chat --provider volcengine\n\n# 使用快速模型(成本更低、响应更快)\nchainlesschain chat --provider volcengine --model doubao-seed-1-6-flash-250828\n\n# 使用代码模型\nchainlesschain chat --provider volcengine --model doubao-seed-code\n```\n\n### 场景 3:使用 OpenAI / DeepSeek 聊天\n\n```bash\n# OpenAI\nexport OPENAI_API_KEY=sk-xxxxxxxxxxxx\nchainlesschain chat --provider openai --model gpt-4o\n\n# DeepSeek\nexport DEEPSEEK_API_KEY=sk-xxxxxxxxxxxx\nchainlesschain chat --provider deepseek --model deepseek-chat\n```\n\n### 场景 4:通过中转站聊天(无需科学上网)\n\n```bash\n# 通过 API2D 中转站使用 GPT-4o\nchainlesschain chat \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n\n# 通过 One-API 网关使用任意模型\nchainlesschain chat \\\n --provider openai \\\n --base-url http://localhost:3000/v1 \\\n --api-key sk-oneapi-xxxxxx \\\n --model deepseek-chat\n\n# 火山引擎通过代理访问\nchainlesschain chat \\\n --provider volcengine \\\n --base-url https://your-proxy.com/volcengine/v3 \\\n --model doubao-seed-1-6-251015\n```\n\n### 场景 5:Agent 模式辅助开发\n\n```bash\n# 本地 Ollama\nchainlesschain chat --agent\n\n# 火山引擎 Agent 模式(自动智能选择模型)\nchainlesschain chat --agent --provider volcengine\n\n# 通过中转站使用 GPT-4o Agent 模式\nchainlesschain chat --agent \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n```\n\n启用代理模式后,AI 可以读写文件、执行命令,适合代码生成、文件修改等开发场景。对话中使用 `/help` 查看可用工具。\n\n### 场景 6:企业自建代理聊天\n\n```bash\n# Nginx 反向代理\nchainlesschain chat \\\n --provider openai \\\n --base-url https://ai-gateway.company.com/v1 \\\n --model gpt-4o\n\n# vLLM 部署的本地模型\nchainlesschain chat \\\n --provider openai \\\n --base-url http://localhost:8000/v1 \\\n --api-key dummy \\\n --model my-finetuned-llama\n```\n\n### 场景 7:脚本集成单次问答 (ask)\n\n```bash\nchainlesschain ask \"将以下 JSON 转为 YAML 格式:{\\\"name\\\":\\\"test\\\"}\" --json\nchainlesschain ask \"解释 WebRTC 的 ICE 候选流程\" --provider openai --api-key sk-xxx\nchainlesschain ask \"什么是机器学习?\" --provider volcengine\n```\n\n非交互模式适合在 CI/CD 流水线或脚本中调用,`--json` 输出便于程序解析。\n\n## 故障排查\n\n| 问题 | 解决方案 |\n| ------------------- | --------------------------------------------------- |\n| 连接 Ollama 超时 | 确认 Ollama 已启动:`ollama serve`,检查端口 11434 |\n| OpenAI API 报错 401 | 检查 API Key 是否正确,是否有余额 |\n| 中转站返回 401 | 检查 `--api-key` 格式,中转站密钥格式可能与官方不同 |\n| 火山引擎认证失败 | 确认 `VOLCENGINE_API_KEY` 以 `ark-` 开头 |\n| 中文乱码 | 确保终端编码为 UTF-8:`chcp 65001` (Windows) |\n| 流式输出卡住 | 检查网络连接,或切换到本地模型 |\n| `--base-url` 不生效 | 确保 URL 包含 `/v1` 后缀 |\n\n## 相关文档\n\n- [LLM 管理](./cli-llm) — 模型列表与提供商管理\n- [LLM 中转站与自定义接入](./cli-llm-proxy) — 中转站、代理、自建网关完整指南\n- [代理模式](./cli-agent) — 带工具调用的高级对话\n- [会话管理](./cli-session) — 对话历史持久化与恢复\n- [Token 追踪](./cli-tokens) — 用量统计与成本分析\n"}}, "docs-site/docs/chainlesschain/cli-llm-proxy.md": {"e8e7ba274b487ed491c04ec3359841a0e545debb": {"t": 1785809876.156188, "n": 18698, "h": "e6b3bb1030002a7d", "c": "# LLM 中转站与自定义接入\n\n> 本指南详细介绍如何通过 `--base-url` 使用 OpenAI 中转站、第三方代理、自建网关接入各种 LLM 服务,以及火山引擎(豆包)和自定义 Provider 的完整使用示例。\n\n## 概述\n\nChainlessChain CLI 所有 AI 命令(`ask`、`chat`、`agent`)均支持 `--base-url` 参数,可将请求转发到任意兼容 OpenAI API 格式的服务端点。这意味着你可以:\n\n- 通过**中转站**(如 OpenAI-SB、API2D、CloseAI 等)使用 GPT-4o\n- 通过**自建 Nginx 反向代理**访问受限 API\n- 使用**企业 API 网关**统一管理密钥\n- 接入任何实现了 `/chat/completions` 接口的自定义 LLM 服务\n\n## 快速上手\n\n30 秒接入中转站:\n\n```bash\n# 1. 从中转站获取 API Key 和 Base URL\n\n# 2. 一行命令即可使用 GPT-4o\nchainlesschain ask \"你好\" \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n```\n\n30 秒接入火山引擎(豆包):\n\n```bash\n# 1. 设置 API Key\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\n\n# 2. 直接使用\nchainlesschain ask \"你好\" --provider volcengine\nchainlesschain chat --provider volcengine\nchainlesschain agent --provider volcengine\n```\n\n---\n\n## 通用参数\n\n所有 AI 命令共享以下参数:\n\n```bash\n--provider # LLM 提供商 (ollama/openai/volcengine/deepseek/...)\n--base-url # API 基础 URL(覆盖提供商默认地址)\n--api-key # API 密钥(覆盖环境变量)\n--model # 模型名称\n```\n\n**关键机制**:`--base-url` 覆盖提供商默认 URL,但保留其 API 格式(OpenAI 兼容)。\n\n---\n\n## 一、OpenAI 中转站接入\n\n### 什么是中转站?\n\n中转站是第三方提供的 OpenAI API 代理服务,通常提供:\n\n- 无需科学上网即可访问 GPT-4o / o1 等模型\n- 按量计费,价格通常低于官方\n- 使用中转站自己的 API Key\n\n### 接入步骤\n\n```bash\n# 1. 从中转站获取 API Key 和 Base URL(以 api2d 为例)\n\n# 2. 单次提问\nchainlesschain ask \"解释量子计算\" \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n\n# 3. 交互式聊天\nchainlesschain chat \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n\n# 4. Agent 模式(可读写文件、执行命令)\nchainlesschain agent \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n```\n\n### 使用环境变量(推荐)\n\n每次输入 `--api-key` 不方便,推荐使用环境变量:\n\n```bash\n# Linux / macOS\nexport OPENAI_API_KEY=fk-xxxxxx\nexport OPENAI_BASE_URL=https://oa.api2d.net/v1\n\n# Windows (PowerShell)\n$env:OPENAI_API_KEY=\"fk-xxxxxx\"\n\n# Windows (CMD)\nset OPENAI_API_KEY=fk-xxxxxx\n\n# 使用时只需指定 base-url\nchainlesschain ask \"什么是 WebRTC?\" \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --model gpt-4o\n```\n\n### 常见中转站示例\n\n| 中转站 | Base URL | 说明 |\n| ---------- | ---------------------------------- | --------------------- |\n| API2D | `https://oa.api2d.net/v1` | 国内可用,支持 GPT-4o |\n| OpenAI-SB | `https://api.openai-sb.com/v1` | 按量计费 |\n| CloseAI | `https://api.closeai-proxy.xyz/v1` | 低延迟 |\n| AiHubMix | `https://aihubmix.com/v1` | 多模型聚合 |\n| 自建 Nginx | `https://your-proxy.com/v1` | 企业内网部署 |\n\n> **注意**:中转站的 URL 格式可能略有差异,请以中转站实际提供的文档为准。部分中转站不需要 `/v1` 后缀。\n\n---\n\n## 二、火山引擎(豆包)使用指南\n\n### 获取 API Key\n\n1. 访问 [火山引擎控制台](https://console.volcengine.com/)\n2. 开通「豆包大模型」服务\n3. 在「API Key 管理」中创建密钥\n\n### 设置环境变量\n\n```bash\n# Linux / macOS\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\n\n# Windows (PowerShell)\n$env:VOLCENGINE_API_KEY=\"ark-xxxxxxxxxxxx\"\n\n# Windows (CMD)\nset VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\n```\n\n### 基础使用\n\n```bash\n# 单次提问(使用旗舰模型)\nchainlesschain ask \"什么是大语言模型?\" --provider volcengine\n\n# 交互式聊天(使用快速模型,成本更低)\nchainlesschain chat --provider volcengine --model doubao-seed-1-6-flash-250828\n\n# Agent 模式(自动根据任务选择最佳模型)\nchainlesschain agent --provider volcengine\n```\n\n### 模型选择指南\n\n| 模型 ID | 适用场景 | 特点 | 推荐用法 |\n| ------------------------------ | -------------- | ------------------ | ------------------ |\n| `doubao-seed-1-6-251015` | 通用/推理/创作 | 旗舰模型,能力最强 | 复杂分析、长文写作 |\n| `doubao-seed-1-6-flash-250828` | 日常对话 | 快速响应,成本低 | 日常问答、闲聊 |\n| `doubao-seed-1-6-lite-251015` | 快速查询 | 延迟最低 | 简单查询、快速回答 |\n| `doubao-seed-code` | 代码编程 | 专用代码模型 | 写代码、debug |\n\n### 任务智能选择\n\nAgent 模式下会自动检测任务类型并选择最佳模型:\n\n```\nyou> 写一个 Python 快速排序函数\n[auto] 代码任务 → doubao-seed-code\nai> def quicksort(arr):\n if len(arr) <= 1:\n return arr\n ...\n\nyou> 分析 TCP 三次握手的过程\n[auto] 复杂推理 → doubao-seed-1-6-251015\nai> TCP三次握手过程如下:\n 1. SYN: 客户端发送 SYN=1, seq=x ...\n\nyou> 今天天气怎么样?\nai> (使用默认 flash 模型,快速响应)\n```\n\n### 火山引擎 + 中转站\n\n如果你在海外或需要通过代理访问火山引擎:\n\n```bash\nchainlesschain chat \\\n --provider volcengine \\\n --base-url https://your-proxy.com/volcengine/v3 \\\n --model doubao-seed-1-6-251015\n```\n\n---\n\n## 三、DeepSeek 使用示例\n\n```bash\n# 设置环境变量\nexport DEEPSEEK_API_KEY=sk-xxxxxxxxxxxx\n\n# 日常对话\nchainlesschain chat --provider deepseek --model deepseek-chat\n\n# 代码任务\nchainlesschain ask \"用 Rust 写一个 HTTP 服务器\" \\\n --provider deepseek --model deepseek-coder\n\n# 通过中转站使用 DeepSeek\nchainlesschain ask \"你好\" \\\n --provider deepseek \\\n --base-url https://your-proxy.com/v1 \\\n --api-key sk-xxxxxx \\\n --model deepseek-chat\n```\n\n---\n\n## 四、Claude (Anthropic) 中转站接入\n\nAnthropic 的 Claude 使用非标准 API 格式(Messages API),但许多中转站提供了 OpenAI 兼容接口:\n\n```bash\n# 方式 1:通过支持 Claude 的 OpenAI 兼容中转站\nchainlesschain ask \"写一首诗\" \\\n --provider openai \\\n --base-url https://your-claude-proxy.com/v1 \\\n --api-key sk-xxxxxx \\\n --model claude-sonnet-4-20250514\n\n# 方式 2:直连 Anthropic API(原生格式)\nexport ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxx\nchainlesschain agent --provider anthropic --model claude-sonnet-4-6\n```\n\n---\n\n## 五、自建代理 / 企业网关\n\n### Nginx 反向代理\n\n如果你需要在企业内网中转 OpenAI API:\n\n```nginx\n# /etc/nginx/conf.d/openai-proxy.conf\nserver {\n listen 443 ssl;\n server_name ai-gateway.company.com;\n\n location /v1/ {\n proxy_pass https://api.openai.com/v1/;\n proxy_set_header Authorization $http_authorization;\n proxy_set_header Content-Type $http_content_type;\n }\n}\n```\n\n```bash\n# 使用企业代理\nchainlesschain agent \\\n --provider openai \\\n --base-url https://ai-gateway.company.com/v1 \\\n --model gpt-4o\n```\n\n### One-API / New-API 网关\n\n[One-API](https://github.com/songquanpeng/one-api) 是常用的多模型聚合网关:\n\n```bash\n# One-API 将多个 LLM 统一为 OpenAI 兼容格式\nchainlesschain chat \\\n --provider openai \\\n --base-url http://localhost:3000/v1 \\\n --api-key sk-oneapi-xxxxxx \\\n --model gpt-4o # 或 claude-3-5-sonnet, deepseek-chat, 等\n\n# 也可以使用 One-API 的渠道切换\nchainlesschain agent \\\n --provider openai \\\n --base-url http://localhost:3000/v1 \\\n --api-key sk-oneapi-xxxxxx \\\n --model deepseek-chat\n```\n\n---\n\n## 六、自定义 Provider(编程接入)\n\n通过 CLI 注册全新的自定义 LLM 提供商:\n\n```bash\n# 添加自定义提供商\nchainlesschain llm add my-llm \\\n --display-name \"My LLM\" \\\n --base-url https://my-llm-service.com/v1 \\\n --api-key-env MY_LLM_KEY \\\n --models \"model-a,model-b\"\n\n# 使用自定义提供商\nexport MY_LLM_KEY=xxxxxx\nchainlesschain chat --provider my-llm --model model-a\n\n# 移除自定义提供商\nchainlesschain llm remove my-llm\n```\n\n### 适用场景\n\n- 企业自建大模型服务\n- vLLM / TGI 等推理框架部署的本地模型\n- 内部 API 网关后的模型服务\n\n```bash\n# vLLM 部署的本地模型\nchainlesschain chat \\\n --provider openai \\\n --base-url http://localhost:8000/v1 \\\n --api-key dummy \\\n --model my-finetuned-llama\n\n# text-generation-inference\nchainlesschain ask \"你好\" \\\n --provider openai \\\n --base-url http://localhost:8080/v1 \\\n --api-key dummy \\\n --model tgi-model\n```\n\n---\n\n## 七、完整配置参考\n\n### 所有内置提供商\n\n| 提供商 | 环境变量 | 默认 Base URL |\n| ------------------ | -------------------- | --------------------------------------------------- |\n| Volcengine (豆包) | `VOLCENGINE_API_KEY` | `https://ark.cn-beijing.volces.com/api/v3` |\n| OpenAI | `OPENAI_API_KEY` | `https://api.openai.com/v1` |\n| Anthropic (Claude) | `ANTHROPIC_API_KEY` | `https://api.anthropic.com/v1` |\n| DeepSeek | `DEEPSEEK_API_KEY` | `https://api.deepseek.com/v1` |\n| DashScope (阿里) | `DASHSCOPE_API_KEY` | `https://dashscope.aliyuncs.com/compatible-mode/v1` |\n| Google Gemini | `GEMINI_API_KEY` | `https://generativelanguage.googleapis.com/v1beta` |\n| Kimi (月之暗面) | `MOONSHOT_API_KEY` | `https://api.moonshot.cn/v1` |\n| MiniMax (海螺AI) | `MINIMAX_API_KEY` | `https://api.minimax.chat/v1` |\n| Mistral AI | `MISTRAL_API_KEY` | `https://api.mistral.ai/v1` |\n| Ollama (本地) | — | `http://localhost:11434` |\n\n### 优先级规则\n\n1. `--api-key` 命令行参数 > 环境变量 > 配置文件\n2. `--base-url` 命令行参数 > 提供商默认 URL\n3. `--model` 命令行参数 > Agent 智能选择 > 提供商默认模型\n\n### 环境变量快速配置(.env 文件)\n\n```bash\n# ~/.bashrc 或 ~/.zshrc 中添加常用配置\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\nexport OPENAI_API_KEY=sk-xxxxxxxxxxxx\nexport ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxx\nexport DEEPSEEK_API_KEY=sk-xxxxxxxxxxxx\nexport MOONSHOT_API_KEY=sk-xxxxxxxxxxxx\nexport MINIMAX_API_KEY=xxxxxxxxxxxx\n```\n\n---\n\n## 核心特性\n\n- **10 大 LLM 提供商** — 内置 Ollama、OpenAI、Anthropic、DeepSeek、Volcengine(豆包)、DashScope(阿里)、Google Gemini、Kimi(月之暗面)、MiniMax(海螺AI)、Mistral AI\n- **3 种中转模式** — 直连官方 API、通过第三方中转站代理、自建 Nginx/One-API 网关转发\n- **`--base-url` 统一覆盖** — 所有 AI 命令(ask/chat/agent)支持 `--base-url` 参数,将请求转发到任意 OpenAI 兼容端点\n- **API 密钥管理** — 支持命令行参数、环境变量、配置文件三级优先级,密钥不落盘到日志\n- **模型智能选择** — Agent 模式下根据任务类型自动选择最优模型(代码任务→代码模型,复杂推理→旗舰模型)\n- **流式输出** — 所有提供商支持 SSE 流式响应,实时输出 token\n- **自定义 Provider 注册** — 通过 `chainlesschain llm add` 注册企业自建模型服务、vLLM、TGI 等推理框架\n\n## 系统架构\n\n```\n用户命令 (ask / chat / agent)\n │\n ▼\n┌──────────────────────────────────┐\n│ CLI AI 命令层 │\n│ --provider / --base-url / │\n│ --api-key / --model │\n└──────────┬───────────────────────┘\n │\n ▼\n┌──────────────────────────────────┐\n│ LLM Provider 路由层 │\n│ ┌─────────────────────────────┐ │\n│ │ 优先级:命令行 > 环境变量 │ │\n│ │ > 配置文件 > 默认值 │ │\n│ └─────────────────────────────┘ │\n└──────────┬───────────────────────┘\n │\n ┌─────┴──────┬──────────┬──────────┐\n ▼ ▼ ▼ ▼\n┌─────────┐ ┌─────────┐ ┌────────┐ ┌─────────┐\n│ Ollama │ │ OpenAI │ │Anthropic│ │ Custom │\n│ (local) │ │ /中转站 │ │ Claude │ │ Provider│\n│ :11434 │ │ /v1 │ │ /v1 │ │ --base │\n└─────────┘ └─────────┘ └────────┘ └─────────┘\n │ │ │ │\n └────────────┴──────────┴──────────┘\n │\n ▼\n LLM API 响应 (SSE 流式 / JSON)\n```\n\n## 配置参考\n\n```bash\n# 通用 CLI 参数(所有 AI 命令均支持)\n--provider # ollama | openai | anthropic | volcengine | deepseek | ...\n--base-url # 覆盖提供商默认地址(用于中转站、自建网关)\n--api-key # 覆盖环境变量中的 API Key\n--model # 覆盖提供商默认模型\n\n# 各提供商环境变量\nexport OLLAMA_HOST=http://localhost:11434 # 本地 Ollama\nexport OPENAI_API_KEY=sk-xxxxxx # OpenAI / 中转站\nexport OPENAI_BASE_URL=https://oa.api2d.net/v1 # OpenAI 默认 Base URL\nexport ANTHROPIC_API_KEY=sk-ant-xxxxxx # Anthropic Claude\nexport VOLCENGINE_API_KEY=ark-xxxxxx # 火山引擎(豆包)\nexport DEEPSEEK_API_KEY=sk-xxxxxx # DeepSeek\nexport DASHSCOPE_API_KEY=sk-xxxxxx # 阿里 DashScope\nexport GEMINI_API_KEY=xxxxxx # Google Gemini\nexport MOONSHOT_API_KEY=sk-xxxxxx # Kimi (月之暗面)\nexport MINIMAX_API_KEY=xxxxxx # MiniMax (海螺AI)\nexport MISTRAL_API_KEY=xxxxxx # Mistral AI\n\n# 自定义 Provider 注册\nchainlesschain llm add \\\n --display-name \"\" \\\n --base-url \\\n --api-key-env \\\n --models \",\"\n\n# 优先级:命令行 --flag > 环境变量 > 配置文件 > 默认值\n```\n\n## 使用示例\n\n```bash\n# 列出所有支持的 LLM 提供商\nchainlesschain llm providers\n\n# 切换活跃提供商\nchainlesschain llm switch anthropic\n\n# 测试当前提供商连通性\nchainlesschain llm test\n\n# 通过中转站使用 GPT-4o\nchainlesschain ask \"解释量子计算\" \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n\n# 使用火山引擎(豆包)\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\nchainlesschain chat --provider volcengine\n\n# 注册自定义提供商(企业自建模型)\nchainlesschain llm add my-llm \\\n --display-name \"My LLM\" \\\n --base-url https://my-llm-service.com/v1 \\\n --api-key-env MY_LLM_KEY \\\n --models \"model-a,model-b\"\n\n# 使用本地 vLLM 部署的模型\nchainlesschain chat \\\n --provider openai \\\n --base-url http://localhost:8000/v1 \\\n --api-key dummy \\\n --model my-finetuned-llama\n```\n\n## 性能指标\n\n### 端到端延迟参考\n\n下表为各提供商在标准网络环境下的典型首 token 延迟(TTFT)和吞吐量(基于 `chainlesschain ask` 单轮 128 token 输出测试):\n\n| 提供商 | 首 token 延迟 (TTFT) | 吞吐量 (tokens/s) | 说明 |\n| ------------------ | -------------------- | ----------------- | -------------------------- |\n| Ollama(本地 7B) | 200–600 ms | 30–80 | 取决于 GPU/CPU 型号 |\n| Ollama(本地 70B) | 1–3 s | 5–15 | 需要 ≥48 GB VRAM |\n| Volcengine(豆包) | 300–800 ms | 40–120 | 国内节点,延迟稳定 |\n| OpenAI(直连) | 500–1500 ms | 30–80 | 受网络环境影响明显 |\n| OpenAI(中转站) | 600–2000 ms | 25–70 | 多一跳代理,延迟略高 |\n| DeepSeek | 400–1000 ms | 50–100 | 国内直连推荐 |\n| Anthropic Claude | 600–1500 ms | 30–70 | Messages API,流式输出稳定 |\n| DashScope(阿里) | 400–900 ms | 40–90 | 国内节点 |\n| Google Gemini | 500–1200 ms | 35–80 | 需要 VPN,延迟受网络影响 |\n\n> **注意**:以上数据为经验参考值,实际延迟受网络状况、模型负载、请求 token 数等因素影响。\n\n### `--base-url` 中转额外开销\n\n使用第三方中转站相比直连官方 API,通常引入以下额外开销:\n\n| 开销来源 | 典型增量 | 缓解方法 |\n| -------------- | ---------- | ------------------------------------------ |\n| 中转站网络跳数 | +50–300 ms | 选择地理位置近的中转站节点 |\n| 中转站请求解析 | +10–50 ms | 影响可忽略 |\n| HTTPS 握手 | +30–100 ms | 连接复用(keep-alive)自动减少后续请求开销 |\n\n### 流式输出 (SSE) 性能\n\n所有提供商均支持 SSE 流式响应,可显著降低感知延迟:\n\n```bash\n# 流式模式(默认启用,实时输出 token)\nchainlesschain ask \"写一篇技术文章\" --provider volcengine\n\n# 非流式模式(等待完整响应后一次性输出)\nchainlesschain ask \"写一篇技术文章\" --provider volcengine --no-stream\n```\n\n流式模式下用户通常在 TTFT 后即可看到内容,感知延迟 = 首 token 延迟,而非总生成时间。\n\n---\n\n## 测试覆盖率\n\nLLM 中转与自定义接入功能通过以下测试模块保证正确性:\n\n| 测试文件 | 用例数 | 覆盖内容 |\n| -------------------------------------------------------------- | ------ | ----------------------------------------------- |\n| `packages/cli/__tests__/unit/llm-providers/index.test.js` | 22 | 提供商注册、路由优先级、环境变量读取 |\n| `packages/cli/__tests__/unit/llm-providers/openai.test.js` | 18 | `--base-url` 覆盖、认证头、SSE 流式解析 |\n| `packages/cli/__tests__/unit/llm-providers/volcengine.test.js` | 15 | 豆包模型映射、任务类型自动选择、`ark-` key 校验 |\n| `packages/cli/__tests__/unit/llm-providers/anthropic.test.js` | 14 | Messages API 格式转换、流式块拼接 |\n| `packages/cli/__tests__/unit/llm-providers/deepseek.test.js` | 12 | DeepSeek 端点、模型别名映射 |\n| `packages/cli/__tests__/unit/commands/llm.test.js` | 20 | `llm add/remove/switch/test` 命令行解析与持久化 |\n\n**总计**: 101 个测试用例,覆盖率 ≥ 90%(提供商适配器层)。\n\n### 运行测试\n\n```bash\n# 全部 LLM provider 单元测试\ncd packages/cli && npx vitest run __tests__/unit/llm-providers/\n\n# 单独测试火山引擎适配器\nnpx vitest run __tests__/unit/llm-providers/volcengine.test.js\n\n# 包含命令层\nnpx vitest run __tests__/unit/llm-providers/ __tests__/unit/commands/llm.test.js\n```\n\n### 测试示例\n\n```javascript\n// openai.test.js 节选 — --base-url 覆盖验证\nit(\"sends request to custom base-url instead of official endpoint\", async () => {\n const adapter = new OpenAIAdapter(\"fk-test\", \"https://oa.api2d.net/v1\");\n const spy = vi.spyOn(adapter, \"_fetchSSE\").mockResolvedValue({ text: \"ok\" });\n await adapter.chat([{ role: \"user\", content: \"hi\" }]);\n expect(spy.mock.calls[0][0]).toMatch(/^https:\\/\\/oa\\.api2d\\.net\\/v1/);\n});\n\n// volcengine.test.js 节选 — 任务类型自动选择\nit(\"selects code model for code task\", async () => {\n const adapter = new VolcengineAdapter(\"ark-test\");\n const model = adapter.selectModelForTask(\"用 Rust 写一个 HTTP 服务器\");\n expect(model).toBe(\"doubao-seed-code\");\n});\n```\n\n---\n\n## 安全考虑\n\n| 安全措施 | 说明 |\n| ---------------- | -------------------------------------------------------------------------------------------------- |\n| API 密钥本地存储 | 密钥通过环境变量或本地配置文件管理,不会传输至第三方服务 |\n| 密钥不落日志 | CLI 输出和日志中自动脱敏 API 密钥,避免泄露 |\n| 端点验证 | `--base-url` 指定的端点需为 HTTPS(中转站)或本地地址(Ollama/vLLM),CLI 不会向未验证端点发送密钥 |\n| 本地模型优先 | 默认使用本地 Ollama 模型,敏感数据不出境;仅在用户显式指定云端提供商时才发送到外部 API |\n| 中转站风险提示 | 第三方中转站由其运营方管理,API 密钥和请求内容经过中转站服务器,用户需自行评估信任度 |\n| 环境变量隔离 | 每个提供商使用独立的环境变量名(`OPENAI_API_KEY`、`ANTHROPIC_API_KEY` 等),避免密钥混用 |\n\n## 关键文件\n\n| 文件 | 说明 |\n| -------------------------------------------------- | ----------------------------------------------------------------- |\n| `packages/cli/src/lib/llm-providers/` | LLM 提供商适配器目录,每个提供商一个文件 |\n| `packages/cli/src/lib/llm-providers/index.js` | 提供商注册表和路由入口 |\n| `packages/cli/src/lib/llm-providers/ollama.js` | Ollama 本地模型适配器 |\n| `packages/cli/src/lib/llm-providers/openai.js` | OpenAI 适配器(同时用于中转站和 One-API) |\n| `packages/cli/src/lib/llm-providers/anthropic.js` | Anthropic Claude 适配器(Messages API 格式) |\n| `packages/cli/src/lib/llm-providers/volcengine.js` | 火山引擎(豆包)适配器 |\n| `packages/cli/src/lib/llm-providers/deepseek.js` | DeepSeek 适配器 |\n| `packages/cli/src/lib/llm-providers/gemini.js` | Google Gemini 适配器 |\n| `packages/cli/src/commands/llm.js` | `chainlesschain llm` 命令入口(providers/switch/test/add/remove) |\n\n## 故障排查\n\n| 问题 | 解决方案 |\n| ---------------------- | ------------------------------------------------------------- |\n| 中转站返回 401 | 检查 `--api-key` 是否正确,注意中转站密钥格式可能与官方不同 |\n| 连接超时 | 检查 `--base-url` 是否可访问,尝试 `curl /models` |\n| 模型不存在 | 中转站可能不支持所有模型,使用 `--model` 指定中转站支持的模型 |\n| `--base-url` 不生效 | 确保 URL 包含 `/v1` 后缀(如 `https://proxy.com/v1`) |\n| Agent 模式工具调用失败 | 部分中转站不支持 function calling,建议使用支持 tools 的模型 |\n| 火山引擎认证失败 | 确认 API Key 以 `ark-` 开头,且已开通豆包模型服务 |\n| 流式输出乱码 | Windows 用户确保终端 UTF-8:`chcp 65001` |\n\n## 相关文档\n\n- [LLM 管理](./cli-llm) — 提供商管理与连通性测试\n- [AI 对话](./cli-chat) — 交互式聊天\n- [代理模式](./cli-agent) — Agent 工具调用\n- [AI 模型配置](./ai-models) — 桌面端模型配置详情\n"}, "4db48500206ff6a789c53c8197933113b06a6097": {"t": 1785809910.3942344, "n": 18698, "h": "e6b3bb1030002a7d", "c": "# LLM 中转站与自定义接入\n\n> 本指南详细介绍如何通过 `--base-url` 使用 OpenAI 中转站、第三方代理、自建网关接入各种 LLM 服务,以及火山引擎(豆包)和自定义 Provider 的完整使用示例。\n\n## 概述\n\nChainlessChain CLI 所有 AI 命令(`ask`、`chat`、`agent`)均支持 `--base-url` 参数,可将请求转发到任意兼容 OpenAI API 格式的服务端点。这意味着你可以:\n\n- 通过**中转站**(如 OpenAI-SB、API2D、CloseAI 等)使用 GPT-4o\n- 通过**自建 Nginx 反向代理**访问受限 API\n- 使用**企业 API 网关**统一管理密钥\n- 接入任何实现了 `/chat/completions` 接口的自定义 LLM 服务\n\n## 快速上手\n\n30 秒接入中转站:\n\n```bash\n# 1. 从中转站获取 API Key 和 Base URL\n\n# 2. 一行命令即可使用 GPT-4o\nchainlesschain ask \"你好\" \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n```\n\n30 秒接入火山引擎(豆包):\n\n```bash\n# 1. 设置 API Key\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\n\n# 2. 直接使用\nchainlesschain ask \"你好\" --provider volcengine\nchainlesschain chat --provider volcengine\nchainlesschain agent --provider volcengine\n```\n\n---\n\n## 通用参数\n\n所有 AI 命令共享以下参数:\n\n```bash\n--provider # LLM 提供商 (ollama/openai/volcengine/deepseek/...)\n--base-url # API 基础 URL(覆盖提供商默认地址)\n--api-key # API 密钥(覆盖环境变量)\n--model # 模型名称\n```\n\n**关键机制**:`--base-url` 覆盖提供商默认 URL,但保留其 API 格式(OpenAI 兼容)。\n\n---\n\n## 一、OpenAI 中转站接入\n\n### 什么是中转站?\n\n中转站是第三方提供的 OpenAI API 代理服务,通常提供:\n\n- 无需科学上网即可访问 GPT-4o / o1 等模型\n- 按量计费,价格通常低于官方\n- 使用中转站自己的 API Key\n\n### 接入步骤\n\n```bash\n# 1. 从中转站获取 API Key 和 Base URL(以 api2d 为例)\n\n# 2. 单次提问\nchainlesschain ask \"解释量子计算\" \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n\n# 3. 交互式聊天\nchainlesschain chat \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n\n# 4. Agent 模式(可读写文件、执行命令)\nchainlesschain agent \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n```\n\n### 使用环境变量(推荐)\n\n每次输入 `--api-key` 不方便,推荐使用环境变量:\n\n```bash\n# Linux / macOS\nexport OPENAI_API_KEY=fk-xxxxxx\nexport OPENAI_BASE_URL=https://oa.api2d.net/v1\n\n# Windows (PowerShell)\n$env:OPENAI_API_KEY=\"fk-xxxxxx\"\n\n# Windows (CMD)\nset OPENAI_API_KEY=fk-xxxxxx\n\n# 使用时只需指定 base-url\nchainlesschain ask \"什么是 WebRTC?\" \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --model gpt-4o\n```\n\n### 常见中转站示例\n\n| 中转站 | Base URL | 说明 |\n| ---------- | ---------------------------------- | --------------------- |\n| API2D | `https://oa.api2d.net/v1` | 国内可用,支持 GPT-4o |\n| OpenAI-SB | `https://api.openai-sb.com/v1` | 按量计费 |\n| CloseAI | `https://api.closeai-proxy.xyz/v1` | 低延迟 |\n| AiHubMix | `https://aihubmix.com/v1` | 多模型聚合 |\n| 自建 Nginx | `https://your-proxy.com/v1` | 企业内网部署 |\n\n> **注意**:中转站的 URL 格式可能略有差异,请以中转站实际提供的文档为准。部分中转站不需要 `/v1` 后缀。\n\n---\n\n## 二、火山引擎(豆包)使用指南\n\n### 获取 API Key\n\n1. 访问 [火山引擎控制台](https://console.volcengine.com/)\n2. 开通「豆包大模型」服务\n3. 在「API Key 管理」中创建密钥\n\n### 设置环境变量\n\n```bash\n# Linux / macOS\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\n\n# Windows (PowerShell)\n$env:VOLCENGINE_API_KEY=\"ark-xxxxxxxxxxxx\"\n\n# Windows (CMD)\nset VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\n```\n\n### 基础使用\n\n```bash\n# 单次提问(使用旗舰模型)\nchainlesschain ask \"什么是大语言模型?\" --provider volcengine\n\n# 交互式聊天(使用快速模型,成本更低)\nchainlesschain chat --provider volcengine --model doubao-seed-1-6-flash-250828\n\n# Agent 模式(自动根据任务选择最佳模型)\nchainlesschain agent --provider volcengine\n```\n\n### 模型选择指南\n\n| 模型 ID | 适用场景 | 特点 | 推荐用法 |\n| ------------------------------ | -------------- | ------------------ | ------------------ |\n| `doubao-seed-1-6-251015` | 通用/推理/创作 | 旗舰模型,能力最强 | 复杂分析、长文写作 |\n| `doubao-seed-1-6-flash-250828` | 日常对话 | 快速响应,成本低 | 日常问答、闲聊 |\n| `doubao-seed-1-6-lite-251015` | 快速查询 | 延迟最低 | 简单查询、快速回答 |\n| `doubao-seed-code` | 代码编程 | 专用代码模型 | 写代码、debug |\n\n### 任务智能选择\n\nAgent 模式下会自动检测任务类型并选择最佳模型:\n\n```\nyou> 写一个 Python 快速排序函数\n[auto] 代码任务 → doubao-seed-code\nai> def quicksort(arr):\n if len(arr) <= 1:\n return arr\n ...\n\nyou> 分析 TCP 三次握手的过程\n[auto] 复杂推理 → doubao-seed-1-6-251015\nai> TCP三次握手过程如下:\n 1. SYN: 客户端发送 SYN=1, seq=x ...\n\nyou> 今天天气怎么样?\nai> (使用默认 flash 模型,快速响应)\n```\n\n### 火山引擎 + 中转站\n\n如果你在海外或需要通过代理访问火山引擎:\n\n```bash\nchainlesschain chat \\\n --provider volcengine \\\n --base-url https://your-proxy.com/volcengine/v3 \\\n --model doubao-seed-1-6-251015\n```\n\n---\n\n## 三、DeepSeek 使用示例\n\n```bash\n# 设置环境变量\nexport DEEPSEEK_API_KEY=sk-xxxxxxxxxxxx\n\n# 日常对话\nchainlesschain chat --provider deepseek --model deepseek-chat\n\n# 代码任务\nchainlesschain ask \"用 Rust 写一个 HTTP 服务器\" \\\n --provider deepseek --model deepseek-coder\n\n# 通过中转站使用 DeepSeek\nchainlesschain ask \"你好\" \\\n --provider deepseek \\\n --base-url https://your-proxy.com/v1 \\\n --api-key sk-xxxxxx \\\n --model deepseek-chat\n```\n\n---\n\n## 四、Claude (Anthropic) 中转站接入\n\nAnthropic 的 Claude 使用非标准 API 格式(Messages API),但许多中转站提供了 OpenAI 兼容接口:\n\n```bash\n# 方式 1:通过支持 Claude 的 OpenAI 兼容中转站\nchainlesschain ask \"写一首诗\" \\\n --provider openai \\\n --base-url https://your-claude-proxy.com/v1 \\\n --api-key sk-xxxxxx \\\n --model claude-sonnet-4-20250514\n\n# 方式 2:直连 Anthropic API(原生格式)\nexport ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxx\nchainlesschain agent --provider anthropic --model claude-sonnet-4-6\n```\n\n---\n\n## 五、自建代理 / 企业网关\n\n### Nginx 反向代理\n\n如果你需要在企业内网中转 OpenAI API:\n\n```nginx\n# /etc/nginx/conf.d/openai-proxy.conf\nserver {\n listen 443 ssl;\n server_name ai-gateway.company.com;\n\n location /v1/ {\n proxy_pass https://api.openai.com/v1/;\n proxy_set_header Authorization $http_authorization;\n proxy_set_header Content-Type $http_content_type;\n }\n}\n```\n\n```bash\n# 使用企业代理\nchainlesschain agent \\\n --provider openai \\\n --base-url https://ai-gateway.company.com/v1 \\\n --model gpt-4o\n```\n\n### One-API / New-API 网关\n\n[One-API](https://github.com/songquanpeng/one-api) 是常用的多模型聚合网关:\n\n```bash\n# One-API 将多个 LLM 统一为 OpenAI 兼容格式\nchainlesschain chat \\\n --provider openai \\\n --base-url http://localhost:3000/v1 \\\n --api-key sk-oneapi-xxxxxx \\\n --model gpt-4o # 或 claude-3-5-sonnet, deepseek-chat, 等\n\n# 也可以使用 One-API 的渠道切换\nchainlesschain agent \\\n --provider openai \\\n --base-url http://localhost:3000/v1 \\\n --api-key sk-oneapi-xxxxxx \\\n --model deepseek-chat\n```\n\n---\n\n## 六、自定义 Provider(编程接入)\n\n通过 CLI 注册全新的自定义 LLM 提供商:\n\n```bash\n# 添加自定义提供商\nchainlesschain llm add my-llm \\\n --display-name \"My LLM\" \\\n --base-url https://my-llm-service.com/v1 \\\n --api-key-env MY_LLM_KEY \\\n --models \"model-a,model-b\"\n\n# 使用自定义提供商\nexport MY_LLM_KEY=xxxxxx\nchainlesschain chat --provider my-llm --model model-a\n\n# 移除自定义提供商\nchainlesschain llm remove my-llm\n```\n\n### 适用场景\n\n- 企业自建大模型服务\n- vLLM / TGI 等推理框架部署的本地模型\n- 内部 API 网关后的模型服务\n\n```bash\n# vLLM 部署的本地模型\nchainlesschain chat \\\n --provider openai \\\n --base-url http://localhost:8000/v1 \\\n --api-key dummy \\\n --model my-finetuned-llama\n\n# text-generation-inference\nchainlesschain ask \"你好\" \\\n --provider openai \\\n --base-url http://localhost:8080/v1 \\\n --api-key dummy \\\n --model tgi-model\n```\n\n---\n\n## 七、完整配置参考\n\n### 所有内置提供商\n\n| 提供商 | 环境变量 | 默认 Base URL |\n| ------------------ | -------------------- | --------------------------------------------------- |\n| Volcengine (豆包) | `VOLCENGINE_API_KEY` | `https://ark.cn-beijing.volces.com/api/v3` |\n| OpenAI | `OPENAI_API_KEY` | `https://api.openai.com/v1` |\n| Anthropic (Claude) | `ANTHROPIC_API_KEY` | `https://api.anthropic.com/v1` |\n| DeepSeek | `DEEPSEEK_API_KEY` | `https://api.deepseek.com/v1` |\n| DashScope (阿里) | `DASHSCOPE_API_KEY` | `https://dashscope.aliyuncs.com/compatible-mode/v1` |\n| Google Gemini | `GEMINI_API_KEY` | `https://generativelanguage.googleapis.com/v1beta` |\n| Kimi (月之暗面) | `MOONSHOT_API_KEY` | `https://api.moonshot.cn/v1` |\n| MiniMax (海螺AI) | `MINIMAX_API_KEY` | `https://api.minimax.chat/v1` |\n| Mistral AI | `MISTRAL_API_KEY` | `https://api.mistral.ai/v1` |\n| Ollama (本地) | — | `http://localhost:11434` |\n\n### 优先级规则\n\n1. `--api-key` 命令行参数 > 环境变量 > 配置文件\n2. `--base-url` 命令行参数 > 提供商默认 URL\n3. `--model` 命令行参数 > Agent 智能选择 > 提供商默认模型\n\n### 环境变量快速配置(.env 文件)\n\n```bash\n# ~/.bashrc 或 ~/.zshrc 中添加常用配置\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\nexport OPENAI_API_KEY=sk-xxxxxxxxxxxx\nexport ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxx\nexport DEEPSEEK_API_KEY=sk-xxxxxxxxxxxx\nexport MOONSHOT_API_KEY=sk-xxxxxxxxxxxx\nexport MINIMAX_API_KEY=xxxxxxxxxxxx\n```\n\n---\n\n## 核心特性\n\n- **10 大 LLM 提供商** — 内置 Ollama、OpenAI、Anthropic、DeepSeek、Volcengine(豆包)、DashScope(阿里)、Google Gemini、Kimi(月之暗面)、MiniMax(海螺AI)、Mistral AI\n- **3 种中转模式** — 直连官方 API、通过第三方中转站代理、自建 Nginx/One-API 网关转发\n- **`--base-url` 统一覆盖** — 所有 AI 命令(ask/chat/agent)支持 `--base-url` 参数,将请求转发到任意 OpenAI 兼容端点\n- **API 密钥管理** — 支持命令行参数、环境变量、配置文件三级优先级,密钥不落盘到日志\n- **模型智能选择** — Agent 模式下根据任务类型自动选择最优模型(代码任务→代码模型,复杂推理→旗舰模型)\n- **流式输出** — 所有提供商支持 SSE 流式响应,实时输出 token\n- **自定义 Provider 注册** — 通过 `chainlesschain llm add` 注册企业自建模型服务、vLLM、TGI 等推理框架\n\n## 系统架构\n\n```\n用户命令 (ask / chat / agent)\n │\n ▼\n┌──────────────────────────────────┐\n│ CLI AI 命令层 │\n│ --provider / --base-url / │\n│ --api-key / --model │\n└──────────┬───────────────────────┘\n │\n ▼\n┌──────────────────────────────────┐\n│ LLM Provider 路由层 │\n│ ┌─────────────────────────────┐ │\n│ │ 优先级:命令行 > 环境变量 │ │\n│ │ > 配置文件 > 默认值 │ │\n│ └─────────────────────────────┘ │\n└──────────┬───────────────────────┘\n │\n ┌─────┴──────┬──────────┬──────────┐\n ▼ ▼ ▼ ▼\n┌─────────┐ ┌─────────┐ ┌────────┐ ┌─────────┐\n│ Ollama │ │ OpenAI │ │Anthropic│ │ Custom │\n│ (local) │ │ /中转站 │ │ Claude │ │ Provider│\n│ :11434 │ │ /v1 │ │ /v1 │ │ --base │\n└─────────┘ └─────────┘ └────────┘ └─────────┘\n │ │ │ │\n └────────────┴──────────┴──────────┘\n │\n ▼\n LLM API 响应 (SSE 流式 / JSON)\n```\n\n## 配置参考\n\n```bash\n# 通用 CLI 参数(所有 AI 命令均支持)\n--provider # ollama | openai | anthropic | volcengine | deepseek | ...\n--base-url # 覆盖提供商默认地址(用于中转站、自建网关)\n--api-key # 覆盖环境变量中的 API Key\n--model # 覆盖提供商默认模型\n\n# 各提供商环境变量\nexport OLLAMA_HOST=http://localhost:11434 # 本地 Ollama\nexport OPENAI_API_KEY=sk-xxxxxx # OpenAI / 中转站\nexport OPENAI_BASE_URL=https://oa.api2d.net/v1 # OpenAI 默认 Base URL\nexport ANTHROPIC_API_KEY=sk-ant-xxxxxx # Anthropic Claude\nexport VOLCENGINE_API_KEY=ark-xxxxxx # 火山引擎(豆包)\nexport DEEPSEEK_API_KEY=sk-xxxxxx # DeepSeek\nexport DASHSCOPE_API_KEY=sk-xxxxxx # 阿里 DashScope\nexport GEMINI_API_KEY=xxxxxx # Google Gemini\nexport MOONSHOT_API_KEY=sk-xxxxxx # Kimi (月之暗面)\nexport MINIMAX_API_KEY=xxxxxx # MiniMax (海螺AI)\nexport MISTRAL_API_KEY=xxxxxx # Mistral AI\n\n# 自定义 Provider 注册\nchainlesschain llm add \\\n --display-name \"\" \\\n --base-url \\\n --api-key-env \\\n --models \",\"\n\n# 优先级:命令行 --flag > 环境变量 > 配置文件 > 默认值\n```\n\n## 使用示例\n\n```bash\n# 列出所有支持的 LLM 提供商\nchainlesschain llm providers\n\n# 切换活跃提供商\nchainlesschain llm switch anthropic\n\n# 测试当前提供商连通性\nchainlesschain llm test\n\n# 通过中转站使用 GPT-4o\nchainlesschain ask \"解释量子计算\" \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n\n# 使用火山引擎(豆包)\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\nchainlesschain chat --provider volcengine\n\n# 注册自定义提供商(企业自建模型)\nchainlesschain llm add my-llm \\\n --display-name \"My LLM\" \\\n --base-url https://my-llm-service.com/v1 \\\n --api-key-env MY_LLM_KEY \\\n --models \"model-a,model-b\"\n\n# 使用本地 vLLM 部署的模型\nchainlesschain chat \\\n --provider openai \\\n --base-url http://localhost:8000/v1 \\\n --api-key dummy \\\n --model my-finetuned-llama\n```\n\n## 性能指标\n\n### 端到端延迟参考\n\n下表为各提供商在标准网络环境下的典型首 token 延迟(TTFT)和吞吐量(基于 `chainlesschain ask` 单轮 128 token 输出测试):\n\n| 提供商 | 首 token 延迟 (TTFT) | 吞吐量 (tokens/s) | 说明 |\n| ------------------ | -------------------- | ----------------- | -------------------------- |\n| Ollama(本地 7B) | 200–600 ms | 30–80 | 取决于 GPU/CPU 型号 |\n| Ollama(本地 70B) | 1–3 s | 5–15 | 需要 ≥48 GB VRAM |\n| Volcengine(豆包) | 300–800 ms | 40–120 | 国内节点,延迟稳定 |\n| OpenAI(直连) | 500–1500 ms | 30–80 | 受网络环境影响明显 |\n| OpenAI(中转站) | 600–2000 ms | 25–70 | 多一跳代理,延迟略高 |\n| DeepSeek | 400–1000 ms | 50–100 | 国内直连推荐 |\n| Anthropic Claude | 600–1500 ms | 30–70 | Messages API,流式输出稳定 |\n| DashScope(阿里) | 400–900 ms | 40–90 | 国内节点 |\n| Google Gemini | 500–1200 ms | 35–80 | 需要 VPN,延迟受网络影响 |\n\n> **注意**:以上数据为经验参考值,实际延迟受网络状况、模型负载、请求 token 数等因素影响。\n\n### `--base-url` 中转额外开销\n\n使用第三方中转站相比直连官方 API,通常引入以下额外开销:\n\n| 开销来源 | 典型增量 | 缓解方法 |\n| -------------- | ---------- | ------------------------------------------ |\n| 中转站网络跳数 | +50–300 ms | 选择地理位置近的中转站节点 |\n| 中转站请求解析 | +10–50 ms | 影响可忽略 |\n| HTTPS 握手 | +30–100 ms | 连接复用(keep-alive)自动减少后续请求开销 |\n\n### 流式输出 (SSE) 性能\n\n所有提供商均支持 SSE 流式响应,可显著降低感知延迟:\n\n```bash\n# 流式模式(默认启用,实时输出 token)\nchainlesschain ask \"写一篇技术文章\" --provider volcengine\n\n# 非流式模式(等待完整响应后一次性输出)\nchainlesschain ask \"写一篇技术文章\" --provider volcengine --no-stream\n```\n\n流式模式下用户通常在 TTFT 后即可看到内容,感知延迟 = 首 token 延迟,而非总生成时间。\n\n---\n\n## 测试覆盖率\n\nLLM 中转与自定义接入功能通过以下测试模块保证正确性:\n\n| 测试文件 | 用例数 | 覆盖内容 |\n| -------------------------------------------------------------- | ------ | ----------------------------------------------- |\n| `packages/cli/__tests__/unit/llm-providers/index.test.js` | 22 | 提供商注册、路由优先级、环境变量读取 |\n| `packages/cli/__tests__/unit/llm-providers/openai.test.js` | 18 | `--base-url` 覆盖、认证头、SSE 流式解析 |\n| `packages/cli/__tests__/unit/llm-providers/volcengine.test.js` | 15 | 豆包模型映射、任务类型自动选择、`ark-` key 校验 |\n| `packages/cli/__tests__/unit/llm-providers/anthropic.test.js` | 14 | Messages API 格式转换、流式块拼接 |\n| `packages/cli/__tests__/unit/llm-providers/deepseek.test.js` | 12 | DeepSeek 端点、模型别名映射 |\n| `packages/cli/__tests__/unit/commands/llm.test.js` | 20 | `llm add/remove/switch/test` 命令行解析与持久化 |\n\n**总计**: 101 个测试用例,覆盖率 ≥ 90%(提供商适配器层)。\n\n### 运行测试\n\n```bash\n# 全部 LLM provider 单元测试\ncd packages/cli && npx vitest run __tests__/unit/llm-providers/\n\n# 单独测试火山引擎适配器\nnpx vitest run __tests__/unit/llm-providers/volcengine.test.js\n\n# 包含命令层\nnpx vitest run __tests__/unit/llm-providers/ __tests__/unit/commands/llm.test.js\n```\n\n### 测试示例\n\n```javascript\n// openai.test.js 节选 — --base-url 覆盖验证\nit(\"sends request to custom base-url instead of official endpoint\", async () => {\n const adapter = new OpenAIAdapter(\"fk-test\", \"https://oa.api2d.net/v1\");\n const spy = vi.spyOn(adapter, \"_fetchSSE\").mockResolvedValue({ text: \"ok\" });\n await adapter.chat([{ role: \"user\", content: \"hi\" }]);\n expect(spy.mock.calls[0][0]).toMatch(/^https:\\/\\/oa\\.api2d\\.net\\/v1/);\n});\n\n// volcengine.test.js 节选 — 任务类型自动选择\nit(\"selects code model for code task\", async () => {\n const adapter = new VolcengineAdapter(\"ark-test\");\n const model = adapter.selectModelForTask(\"用 Rust 写一个 HTTP 服务器\");\n expect(model).toBe(\"doubao-seed-code\");\n});\n```\n\n---\n\n## 安全考虑\n\n| 安全措施 | 说明 |\n| ---------------- | -------------------------------------------------------------------------------------------------- |\n| API 密钥本地存储 | 密钥通过环境变量或本地配置文件管理,不会传输至第三方服务 |\n| 密钥不落日志 | CLI 输出和日志中自动脱敏 API 密钥,避免泄露 |\n| 端点验证 | `--base-url` 指定的端点需为 HTTPS(中转站)或本地地址(Ollama/vLLM),CLI 不会向未验证端点发送密钥 |\n| 本地模型优先 | 默认使用本地 Ollama 模型,敏感数据不出境;仅在用户显式指定云端提供商时才发送到外部 API |\n| 中转站风险提示 | 第三方中转站由其运营方管理,API 密钥和请求内容经过中转站服务器,用户需自行评估信任度 |\n| 环境变量隔离 | 每个提供商使用独立的环境变量名(`OPENAI_API_KEY`、`ANTHROPIC_API_KEY` 等),避免密钥混用 |\n\n## 关键文件\n\n| 文件 | 说明 |\n| -------------------------------------------------- | ----------------------------------------------------------------- |\n| `packages/cli/src/lib/llm-providers/` | LLM 提供商适配器目录,每个提供商一个文件 |\n| `packages/cli/src/lib/llm-providers/index.js` | 提供商注册表和路由入口 |\n| `packages/cli/src/lib/llm-providers/ollama.js` | Ollama 本地模型适配器 |\n| `packages/cli/src/lib/llm-providers/openai.js` | OpenAI 适配器(同时用于中转站和 One-API) |\n| `packages/cli/src/lib/llm-providers/anthropic.js` | Anthropic Claude 适配器(Messages API 格式) |\n| `packages/cli/src/lib/llm-providers/volcengine.js` | 火山引擎(豆包)适配器 |\n| `packages/cli/src/lib/llm-providers/deepseek.js` | DeepSeek 适配器 |\n| `packages/cli/src/lib/llm-providers/gemini.js` | Google Gemini 适配器 |\n| `packages/cli/src/commands/llm.js` | `chainlesschain llm` 命令入口(providers/switch/test/add/remove) |\n\n## 故障排查\n\n| 问题 | 解决方案 |\n| ---------------------- | ------------------------------------------------------------- |\n| 中转站返回 401 | 检查 `--api-key` 是否正确,注意中转站密钥格式可能与官方不同 |\n| 连接超时 | 检查 `--base-url` 是否可访问,尝试 `curl /models` |\n| 模型不存在 | 中转站可能不支持所有模型,使用 `--model` 指定中转站支持的模型 |\n| `--base-url` 不生效 | 确保 URL 包含 `/v1` 后缀(如 `https://proxy.com/v1`) |\n| Agent 模式工具调用失败 | 部分中转站不支持 function calling,建议使用支持 tools 的模型 |\n| 火山引擎认证失败 | 确认 API Key 以 `ark-` 开头,且已开通豆包模型服务 |\n| 流式输出乱码 | Windows 用户确保终端 UTF-8:`chcp 65001` |\n\n## 相关文档\n\n- [LLM 管理](./cli-llm) — 提供商管理与连通性测试\n- [AI 对话](./cli-chat) — 交互式聊天\n- [代理模式](./cli-agent) — Agent 工具调用\n- [AI 模型配置](./ai-models) — 桌面端模型配置详情\n"}}, "docs-site/docs/chainlesschain/cli-ask.md": {"e8e7ba274b487ed491c04ec3359841a0e545debb": {"t": 1785809876.1794052, "n": 7593, "h": "f6bf13d26c09952a", "c": "# ask 命令\n\n> 单次 AI 问答 — 向 LLM 提供商发送一个问题并获取回答\n\n## 概述\n\nask 命令提供单次 AI 问答能力,无需进入交互式对话即可快速获取 LLM 回答。支持 Ollama、OpenAI、Anthropic、DeepSeek 等 8 个提供商,可通过 `--base-url` 接入任意 OpenAI 兼容中转站,支持 JSON 格式输出便于脚本集成。\n\n## 核心特性\n\n- 🔹 **单次问答**: 无需进入交互式对话,一条命令即可获得 AI 回答\n- 🔹 **8 个提供商**: 支持 Ollama、OpenAI、Anthropic、DeepSeek、DashScope、Gemini、Mistral、Volcengine(豆包)\n- 🔹 **中转站支持**: 通过 `--base-url` 接入任意 OpenAI 兼容中转站/代理\n- 🔹 **模型可选**: 通过 `--model` 参数指定任意已安装模型\n- 🔹 **JSON 输出**: 支持 `--json` 格式化输出,便于脚本集成\n- 🔹 **API 密钥管理**: 支持命令行参数或环境变量传入 API 密钥\n\n## 系统架构\n\n```\n用户输入问题\n │\n ▼\n┌─────────────┐\n│ ask 命令 │\n│ 参数解析 │\n└──────┬──────┘\n │\n ▼\n┌─────────────┐ ┌──────────────┐\n│ queryLLM() │───▶│ Ollama API │ POST /api/generate\n│ 路由分发 │ │ (本地 11434) │\n│ │ └──────────────┘\n│ │ ┌──────────────────────────────────┐\n│ │───▶│ OpenAI 兼容 API (8 providers) │\n│ │ │ openai/volcengine/deepseek/... │\n│ │ │ POST /chat/completions │\n│ │ │ --base-url 可覆盖为中转站地址 │\n└──────┬──────┘ └──────────────────────────────────┘\n │\n ▼\n┌─────────────┐\n│ 格式化输出 │ 文本 / JSON\n└─────────────┘\n```\n\n## 用法\n\n```bash\nchainlesschain ask [options]\n```\n\n### 参数\n\n| 参数 | 说明 |\n| ------------ | -------------------- |\n| `` | 要提问的问题(必填) |\n\n### 选项\n\n| 选项 | 说明 | 默认值 |\n| ----------------------- | --------------------------------------------------------------------------------- | ---------------------- |\n| `--model ` | 模型名称 | `qwen2:7b` |\n| `--provider ` | LLM 提供商 (ollama/openai/volcengine/deepseek/dashscope/gemini/mistral/anthropic) | `ollama` |\n| `--base-url ` | API 基础地址(可覆盖为中转站/代理地址) | 根据 provider 自动选择 |\n| `--api-key ` | API 密钥 | 从对应环境变量读取 |\n| `--json` | 以 JSON 格式输出结果 | — |\n\n## 配置参考\n\n```bash\nchainlesschain ask [options]\n\n参数:\n 要提问的问题(必填)\n\n选项:\n --model 模型名称(默认: qwen2:7b)\n --provider LLM 提供商 (ollama/openai/volcengine/deepseek/\n dashscope/gemini/mistral/anthropic) 默认: ollama\n --base-url API 基础地址(可覆盖为中转站/代理地址)\n --api-key API 密钥(也可通过对应环境变量传入)\n --json 以 JSON 格式输出结果\n\n环境变量:\n OPENAI_API_KEY OpenAI 密钥\n ANTHROPIC_API_KEY Anthropic 密钥\n DEEPSEEK_API_KEY DeepSeek 密钥\n DASHSCOPE_API_KEY 阿里 DashScope 密钥\n GEMINI_API_KEY Google Gemini 密钥\n MISTRAL_API_KEY Mistral AI 密钥\n VOLCENGINE_API_KEY Volcengine (豆包) 密钥\n```\n\n## 性能指标\n\n| 操作 | 目标 | 实际 | 状态 |\n| --------------------------------- | ------ | ----- | ---- |\n| 参数解析与 provider 路由 | < 20ms | ~8ms | ✅ |\n| Ollama 本地响应(qwen2:7b) | < 3s | ~1.5s | ✅ |\n| 云端 API 响应(gpt-4o) | < 5s | ~2s | ✅ |\n| JSON 输出序列化 | < 10ms | ~3ms | ✅ |\n| 中转站请求透传 | < 5s | ~2.5s | ✅ |\n| API Key 读取(环境变量 fallback) | < 5ms | ~1ms | ✅ |\n\n## 测试覆盖率\n\n```\n✅ ask.test.js - 覆盖 ask CLI 的主要路径\n ├── 参数解析 / 选项验证(provider/model/base-url/api-key)\n ├── 正常路径(Ollama、OpenAI、中转站多 provider)\n ├── 错误处理 / 边界情况(缺失 API Key、provider 不支持、连接失败)\n └── JSON 输出格式\n```\n\n## 关键文件\n\n- `packages/cli/src/commands/ask.js` — 命令实现,包含 `queryLLM()` 函数和命令注册\n- `packages/cli/src/lib/llm-providers.js` — 8 个 LLM 提供商适配层\n- `packages/cli/src/lib/logger.js` — 日志输出工具\n\n## 安全考虑\n\n- API 密钥优先从环境变量读取,避免在命令行中暴露\n- 使用 `--api-key` 参数传入密钥时,密钥可能出现在进程列表和 shell 历史中\n- Ollama 默认使用本地服务,数据不会发送到外部\n\n## 使用示例\n\n### 场景 1:使用本地 Ollama 提问\n\n```bash\nchainlesschain ask \"什么是区块链?\"\nchainlesschain ask \"解释量子计算\" --model llama3:8b\n```\n\n### 场景 2:使用火山引擎(豆包)\n\n```bash\n# 设置环境变量(推荐)\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\n\n# 单次提问(使用旗舰模型)\nchainlesschain ask \"什么是大语言模型?\" --provider volcengine\n\n# 指定模型\nchainlesschain ask \"写一个快速排序\" --provider volcengine --model doubao-seed-code\n\n# 使用快速模型(更低成本)\nchainlesschain ask \"今天星期几?\" --provider volcengine --model doubao-seed-1-6-lite-251015\n```\n\n### 场景 3:使用 OpenAI\n\n```bash\nexport OPENAI_API_KEY=sk-xxxxxxxxxxxx\nchainlesschain ask \"写一首诗\" --provider openai --model gpt-4o\n```\n\n### 场景 4:使用 DeepSeek\n\n```bash\nexport DEEPSEEK_API_KEY=sk-xxxxxxxxxxxx\nchainlesschain ask \"用 Rust 写一个 HTTP 服务器\" --provider deepseek --model deepseek-coder\n```\n\n### 场景 5:通过中转站使用 GPT-4o(无需科学上网)\n\n```bash\n# API2D 中转站\nchainlesschain ask \"解释量子计算\" \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n\n# OpenAI-SB 中转站\nchainlesschain ask \"你好\" \\\n --provider openai \\\n --base-url https://api.openai-sb.com/v1 \\\n --api-key sb-xxxxxx \\\n --model gpt-4o\n\n# One-API 网关(本地部署的多模型聚合)\nchainlesschain ask \"test\" \\\n --provider openai \\\n --base-url http://localhost:3000/v1 \\\n --api-key sk-oneapi-xxxxxx \\\n --model gpt-4o\n```\n\n### 场景 6:通过中转站使用 DeepSeek / Claude\n\n```bash\n# 中转站使用 DeepSeek\nchainlesschain ask \"你好\" \\\n --provider deepseek \\\n --base-url https://your-proxy.com/v1 \\\n --api-key sk-xxxxxx \\\n --model deepseek-chat\n\n# 通过 OpenAI 兼容中转站使用 Claude\nchainlesschain ask \"写一首诗\" \\\n --provider openai \\\n --base-url https://your-claude-proxy.com/v1 \\\n --api-key sk-xxxxxx \\\n --model claude-sonnet-4-20250514\n```\n\n### 场景 7:企业自建代理\n\n```bash\n# Nginx 反向代理\nchainlesschain ask \"分析一下\" \\\n --provider openai \\\n --base-url https://ai-gateway.company.com/v1 \\\n --model gpt-4o\n\n# vLLM 部署的本地模型\nchainlesschain ask \"你好\" \\\n --provider openai \\\n --base-url http://localhost:8000/v1 \\\n --api-key dummy \\\n --model my-finetuned-llama\n```\n\n### 场景 8:JSON 格式输出(用于脚本集成)\n\n```bash\nchainlesschain ask \"今天天气如何?\" --json\n```\n\n输出示例:\n\n```json\n{\n \"question\": \"今天天气如何?\",\n \"answer\": \"...\",\n \"model\": \"qwen2:7b\",\n \"provider\": \"ollama\"\n}\n```\n\n## 环境变量\n\n| 变量名 | 提供商 |\n| -------------------- | ----------------- |\n| `OPENAI_API_KEY` | OpenAI |\n| `ANTHROPIC_API_KEY` | Anthropic |\n| `DEEPSEEK_API_KEY` | DeepSeek |\n| `DASHSCOPE_API_KEY` | DashScope (阿里) |\n| `GEMINI_API_KEY` | Google Gemini |\n| `MISTRAL_API_KEY` | Mistral AI |\n| `VOLCENGINE_API_KEY` | Volcengine (豆包) |\n\n## 故障排查\n\n| 问题 | 解决方案 |\n| ---------------------------------- | ------------------------------------------------------------- |\n| `Ollama error: 404` | 模型未安装,运行 `ollama pull qwen2:7b` |\n| `Ollama error: connection refused` | Ollama 服务未启动,运行 `ollama serve` |\n| `API key required for volcengine` | 设置 `VOLCENGINE_API_KEY` 或使用 `--api-key` |\n| `Unsupported provider` | 支持 8 个提供商,使用 `chainlesschain llm providers` 查看列表 |\n| 中转站返回 401 | 检查 `--api-key` 格式,中转站密钥格式可能与官方不同 |\n| `--base-url` 不生效 | 确保 URL 包含 `/v1` 后缀 |\n| 响应较慢 | 本地模型受硬件限制,可尝试使用云端 API 或更小的模型 |\n\n## 相关文档\n\n- [chat 命令](./cli-chat) — 交互式 AI 对话(支持流式输出)\n- [agent 命令](./cli-agent) — Agentic AI 会话(支持工具调用)\n- [llm 命令](./cli-llm) — LLM 模型和提供商管理\n- [LLM 中转站与自定义接入](./cli-llm-proxy) — 中转站、代理、自建网关完整指南\n"}, "4db48500206ff6a789c53c8197933113b06a6097": {"t": 1785809909.9499352, "n": 7593, "h": "f6bf13d26c09952a", "c": "# ask 命令\n\n> 单次 AI 问答 — 向 LLM 提供商发送一个问题并获取回答\n\n## 概述\n\nask 命令提供单次 AI 问答能力,无需进入交互式对话即可快速获取 LLM 回答。支持 Ollama、OpenAI、Anthropic、DeepSeek 等 8 个提供商,可通过 `--base-url` 接入任意 OpenAI 兼容中转站,支持 JSON 格式输出便于脚本集成。\n\n## 核心特性\n\n- 🔹 **单次问答**: 无需进入交互式对话,一条命令即可获得 AI 回答\n- 🔹 **8 个提供商**: 支持 Ollama、OpenAI、Anthropic、DeepSeek、DashScope、Gemini、Mistral、Volcengine(豆包)\n- 🔹 **中转站支持**: 通过 `--base-url` 接入任意 OpenAI 兼容中转站/代理\n- 🔹 **模型可选**: 通过 `--model` 参数指定任意已安装模型\n- 🔹 **JSON 输出**: 支持 `--json` 格式化输出,便于脚本集成\n- 🔹 **API 密钥管理**: 支持命令行参数或环境变量传入 API 密钥\n\n## 系统架构\n\n```\n用户输入问题\n │\n ▼\n┌─────────────┐\n│ ask 命令 │\n│ 参数解析 │\n└──────┬──────┘\n │\n ▼\n┌─────────────┐ ┌──────────────┐\n│ queryLLM() │───▶│ Ollama API │ POST /api/generate\n│ 路由分发 │ │ (本地 11434) │\n│ │ └──────────────┘\n│ │ ┌──────────────────────────────────┐\n│ │───▶│ OpenAI 兼容 API (8 providers) │\n│ │ │ openai/volcengine/deepseek/... │\n│ │ │ POST /chat/completions │\n│ │ │ --base-url 可覆盖为中转站地址 │\n└──────┬──────┘ └──────────────────────────────────┘\n │\n ▼\n┌─────────────┐\n│ 格式化输出 │ 文本 / JSON\n└─────────────┘\n```\n\n## 用法\n\n```bash\nchainlesschain ask [options]\n```\n\n### 参数\n\n| 参数 | 说明 |\n| ------------ | -------------------- |\n| `` | 要提问的问题(必填) |\n\n### 选项\n\n| 选项 | 说明 | 默认值 |\n| ----------------------- | --------------------------------------------------------------------------------- | ---------------------- |\n| `--model ` | 模型名称 | `qwen2:7b` |\n| `--provider ` | LLM 提供商 (ollama/openai/volcengine/deepseek/dashscope/gemini/mistral/anthropic) | `ollama` |\n| `--base-url ` | API 基础地址(可覆盖为中转站/代理地址) | 根据 provider 自动选择 |\n| `--api-key ` | API 密钥 | 从对应环境变量读取 |\n| `--json` | 以 JSON 格式输出结果 | — |\n\n## 配置参考\n\n```bash\nchainlesschain ask [options]\n\n参数:\n 要提问的问题(必填)\n\n选项:\n --model 模型名称(默认: qwen2:7b)\n --provider LLM 提供商 (ollama/openai/volcengine/deepseek/\n dashscope/gemini/mistral/anthropic) 默认: ollama\n --base-url API 基础地址(可覆盖为中转站/代理地址)\n --api-key API 密钥(也可通过对应环境变量传入)\n --json 以 JSON 格式输出结果\n\n环境变量:\n OPENAI_API_KEY OpenAI 密钥\n ANTHROPIC_API_KEY Anthropic 密钥\n DEEPSEEK_API_KEY DeepSeek 密钥\n DASHSCOPE_API_KEY 阿里 DashScope 密钥\n GEMINI_API_KEY Google Gemini 密钥\n MISTRAL_API_KEY Mistral AI 密钥\n VOLCENGINE_API_KEY Volcengine (豆包) 密钥\n```\n\n## 性能指标\n\n| 操作 | 目标 | 实际 | 状态 |\n| --------------------------------- | ------ | ----- | ---- |\n| 参数解析与 provider 路由 | < 20ms | ~8ms | ✅ |\n| Ollama 本地响应(qwen2:7b) | < 3s | ~1.5s | ✅ |\n| 云端 API 响应(gpt-4o) | < 5s | ~2s | ✅ |\n| JSON 输出序列化 | < 10ms | ~3ms | ✅ |\n| 中转站请求透传 | < 5s | ~2.5s | ✅ |\n| API Key 读取(环境变量 fallback) | < 5ms | ~1ms | ✅ |\n\n## 测试覆盖率\n\n```\n✅ ask.test.js - 覆盖 ask CLI 的主要路径\n ├── 参数解析 / 选项验证(provider/model/base-url/api-key)\n ├── 正常路径(Ollama、OpenAI、中转站多 provider)\n ├── 错误处理 / 边界情况(缺失 API Key、provider 不支持、连接失败)\n └── JSON 输出格式\n```\n\n## 关键文件\n\n- `packages/cli/src/commands/ask.js` — 命令实现,包含 `queryLLM()` 函数和命令注册\n- `packages/cli/src/lib/llm-providers.js` — 8 个 LLM 提供商适配层\n- `packages/cli/src/lib/logger.js` — 日志输出工具\n\n## 安全考虑\n\n- API 密钥优先从环境变量读取,避免在命令行中暴露\n- 使用 `--api-key` 参数传入密钥时,密钥可能出现在进程列表和 shell 历史中\n- Ollama 默认使用本地服务,数据不会发送到外部\n\n## 使用示例\n\n### 场景 1:使用本地 Ollama 提问\n\n```bash\nchainlesschain ask \"什么是区块链?\"\nchainlesschain ask \"解释量子计算\" --model llama3:8b\n```\n\n### 场景 2:使用火山引擎(豆包)\n\n```bash\n# 设置环境变量(推荐)\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\n\n# 单次提问(使用旗舰模型)\nchainlesschain ask \"什么是大语言模型?\" --provider volcengine\n\n# 指定模型\nchainlesschain ask \"写一个快速排序\" --provider volcengine --model doubao-seed-code\n\n# 使用快速模型(更低成本)\nchainlesschain ask \"今天星期几?\" --provider volcengine --model doubao-seed-1-6-lite-251015\n```\n\n### 场景 3:使用 OpenAI\n\n```bash\nexport OPENAI_API_KEY=sk-xxxxxxxxxxxx\nchainlesschain ask \"写一首诗\" --provider openai --model gpt-4o\n```\n\n### 场景 4:使用 DeepSeek\n\n```bash\nexport DEEPSEEK_API_KEY=sk-xxxxxxxxxxxx\nchainlesschain ask \"用 Rust 写一个 HTTP 服务器\" --provider deepseek --model deepseek-coder\n```\n\n### 场景 5:通过中转站使用 GPT-4o(无需科学上网)\n\n```bash\n# API2D 中转站\nchainlesschain ask \"解释量子计算\" \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n\n# OpenAI-SB 中转站\nchainlesschain ask \"你好\" \\\n --provider openai \\\n --base-url https://api.openai-sb.com/v1 \\\n --api-key sb-xxxxxx \\\n --model gpt-4o\n\n# One-API 网关(本地部署的多模型聚合)\nchainlesschain ask \"test\" \\\n --provider openai \\\n --base-url http://localhost:3000/v1 \\\n --api-key sk-oneapi-xxxxxx \\\n --model gpt-4o\n```\n\n### 场景 6:通过中转站使用 DeepSeek / Claude\n\n```bash\n# 中转站使用 DeepSeek\nchainlesschain ask \"你好\" \\\n --provider deepseek \\\n --base-url https://your-proxy.com/v1 \\\n --api-key sk-xxxxxx \\\n --model deepseek-chat\n\n# 通过 OpenAI 兼容中转站使用 Claude\nchainlesschain ask \"写一首诗\" \\\n --provider openai \\\n --base-url https://your-claude-proxy.com/v1 \\\n --api-key sk-xxxxxx \\\n --model claude-sonnet-4-20250514\n```\n\n### 场景 7:企业自建代理\n\n```bash\n# Nginx 反向代理\nchainlesschain ask \"分析一下\" \\\n --provider openai \\\n --base-url https://ai-gateway.company.com/v1 \\\n --model gpt-4o\n\n# vLLM 部署的本地模型\nchainlesschain ask \"你好\" \\\n --provider openai \\\n --base-url http://localhost:8000/v1 \\\n --api-key dummy \\\n --model my-finetuned-llama\n```\n\n### 场景 8:JSON 格式输出(用于脚本集成)\n\n```bash\nchainlesschain ask \"今天天气如何?\" --json\n```\n\n输出示例:\n\n```json\n{\n \"question\": \"今天天气如何?\",\n \"answer\": \"...\",\n \"model\": \"qwen2:7b\",\n \"provider\": \"ollama\"\n}\n```\n\n## 环境变量\n\n| 变量名 | 提供商 |\n| -------------------- | ----------------- |\n| `OPENAI_API_KEY` | OpenAI |\n| `ANTHROPIC_API_KEY` | Anthropic |\n| `DEEPSEEK_API_KEY` | DeepSeek |\n| `DASHSCOPE_API_KEY` | DashScope (阿里) |\n| `GEMINI_API_KEY` | Google Gemini |\n| `MISTRAL_API_KEY` | Mistral AI |\n| `VOLCENGINE_API_KEY` | Volcengine (豆包) |\n\n## 故障排查\n\n| 问题 | 解决方案 |\n| ---------------------------------- | ------------------------------------------------------------- |\n| `Ollama error: 404` | 模型未安装,运行 `ollama pull qwen2:7b` |\n| `Ollama error: connection refused` | Ollama 服务未启动,运行 `ollama serve` |\n| `API key required for volcengine` | 设置 `VOLCENGINE_API_KEY` 或使用 `--api-key` |\n| `Unsupported provider` | 支持 8 个提供商,使用 `chainlesschain llm providers` 查看列表 |\n| 中转站返回 401 | 检查 `--api-key` 格式,中转站密钥格式可能与官方不同 |\n| `--base-url` 不生效 | 确保 URL 包含 `/v1` 后缀 |\n| 响应较慢 | 本地模型受硬件限制,可尝试使用云端 API 或更小的模型 |\n\n## 相关文档\n\n- [chat 命令](./cli-chat) — 交互式 AI 对话(支持流式输出)\n- [agent 命令](./cli-agent) — Agentic AI 会话(支持工具调用)\n- [llm 命令](./cli-llm) — LLM 模型和提供商管理\n- [LLM 中转站与自定义接入](./cli-llm-proxy) — 中转站、代理、自建网关完整指南\n"}}, "docs-site/docs/chainlesschain/cli-agent.md": {"e8e7ba274b487ed491c04ec3359841a0e545debb": {"t": 1785809876.2847757, "n": 41992, "h": "29c5f47d6ce20f1a", "c": "# 代理模式 (agent)\n\n> Headless 命令 — 不依赖桌面 GUI,直接使用核心包运行。适用于服务器、CI/CD、容器化等无桌面环境。\n\n## 核心特性\n\n- 🤖 **Claude Code 风格**: 代理式 AI 会话,自主完成任务\n- 🔧 **25 个内置工具**: 读写/编辑文件、执行命令(含后台)、搜索代码库与会话、代码执行、联网搜索/抓取、Jupyter 编辑、子代理、技能调用、LSP 代码智能、通知/定时意图、交付物发布、浏览器状态观察等\n- 🎯 **146 个技能**: 集成全部内置技能\n- 📋 **Plan Mode**: AI 制定计划,用户审批后执行\n- 💾 **会话持久化**: 自动保存,支持 `--session` 断点恢复\n- 🧠 **Context Engineering**: 6 维上下文注入(Instinct / Memory / BM25 Notes / Task / Permanent Memory / Compaction Summary)\n- 🗜️ **智能压缩**: 基于重要性评分的对话压缩(替代硬编码截断)\n- 🔗 **Hook 管道**: PreToolUse/PostToolUse/ToolError 钩子集成\n- 🤖 **自主模式**: /auto 命令提交目标,AI 自动分解执行\n- ⚠️ **DAG 执行 + 风险评估**: /plan execute 按依赖拓扑排序执行,/plan risk 显示风险评分\n- 🎯 **SlotFiller 意图检测**: 自动检测用户消息中的缺失参数,交互式补全后再调用 LLM\n- 🎭 **Persona 系统**: 项目级 AI 角色配置,自动替换默认编码助手,支持工具权限控制\n- 🎨 **输出风格**: 命名人格叠加系统提示(`--output-style`),保留编码能力改行为/语气,详见 [输出风格](./output-styles)\n\n## 系统架构\n\n```\nagent 命令 → agent.js (Commander) → agent-repl.js\n │\n ┌──────────┬──────────┬─────────────┼──────────────┬─────────────┬──────────┐\n ▼ ▼ ▼ ▼ ▼ ▼ ▼\n 工具系统 Plan Mode Hook管道 Context Engine 会话管理 Autonomous Bootstrap\n (19 tools) (plan-mode) (hook- (cli-context- (session- Agent (bootstrap.js)\n manager) engineering.js) manager) (ReAct) │\n │ │ │ │ │ │ 7-stage init\n ▼ ▼ ▼ ▼ ▼ ▼ (DB/Config)\n read/write 只读→计划→ PreToolUse 6维注入: 自动保存 /auto\n edit/shell 审批→执行 PostToolUse Instinct 到SQLite 目标分解\n search/code DAG执行 ToolError Memory 断点恢复 ReAct循环\n skill/list 风险评估 BM25 Notes 自动纠错\n Task重述\n Permanent Memory\n Compaction Summary\n Multi-Provider (10 LLM)\n Task Model Selector\n```\n\n## 概述\n\n启动 Claude Code 风格的代理式 AI 会话。AI 可读写文件、执行命令、搜索代码库、运行代码脚本(Python/Node.js/Bash)、调用 146 个内置技能。\n支持 8 个 LLM 提供商(ollama/anthropic/openai/deepseek/dashscope/mistral/gemini/volcengine)和自主模式(/auto)。Agent 模式下自动根据任务类型智能选择最佳模型。\n通过 6 维 Context Engineering 自动注入用户偏好(Instinct)、相关记忆(Hierarchical Memory)、相关笔记(BM25 搜索)、任务目标提醒、跨会话持久记忆(Permanent Memory)和压缩摘要(Compaction Summary),使 AI 保持上下文聚焦。\n\n**Persona 支持**: 当项目配置了 Persona(通过 `init --template medical-triage` 或 `persona set`),Agent 自动使用自定义角色替代默认编码助手,并按 `toolsDisabled` 过滤可用工具。详见 [Persona 命令](./cli-persona)。\n\n## 命令参考\n\n```bash\nchainlesschain agent # 默认读 ~/.chainlesschain/config.json 的 llm 配置;无配置时 Ollama qwen2.5:7b\nchainlesschain a --model llama3 # 短别名\nchainlesschain agent --provider openai --api-key sk-...\nchainlesschain agent --session # 恢复历史会话\n```\n\n> `agent` 命令的短别名为 `a`,两者完全等价。\n>\n> **配置默认(v0.162.43+)**:不带 `--provider/--model` 的裸 `cc agent` 会读取 `config.json` 的 `llm` 段作为默认(provider/model/apiKey/baseUrl),与 `cc chat`/桌面端一致;显式 flag 仍然优先。\n\n### 命令选项\n\n| 选项 | 说明 | 默认值 |\n| ----------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------ |\n| `--model ` | LLM 模型名称 | `qwen2.5:7b` |\n| `--provider ` | LLM 提供商(ollama/anthropic/openai/deepseek/dashscope/mistral/gemini/volcengine) | `ollama` |\n| `--base-url ` | API 基础 URL | `http://localhost:11434` |\n| `--api-key ` | API 密钥 | — |\n| `--session ` | 恢复历史会话 | — |\n| `--add-dir

` | 额外工作根(可重复) | — |\n| `--mcp-config ` | 临时挂载 MCP server,工具 → `mcp____` | — |\n| `--no-mcp` | 不自动连 `cc mcp add --auto-connect` 的服务 | (默认连) |\n| `--settings ` | 一次性 `.claude/settings.json`:权限规则 + `model`/`env` 覆盖 | — |\n| `--output-style ` | 套用命名人格(内置 `explanatory`/`learning` 或 `.claude/output-styles/.md`),详见 [输出风格](./output-styles) | — |\n\n## Headless 模式 (`agent -p`) 与 Claude Code 平价旗标\n\n`cc agent -p` 是**单轮非交互**执行(`claude -p` 对标),进 CI / 管道 / 脚本;prompt 来自\n`-p` 值 / 位置参数 / stdin。完整旗标 + 输出格式 + 权限档表见\n[CLI_COMMANDS_REFERENCE → managed-agents](https://github.com/chainlesschain/chainlesschain/blob/main/docs/cli/managed-agents.md)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"...\" --output-format json|stream-json # 机器可读 / 逐事件 NDJSON\nchainlesschain agent -p \"...\" --max-turns 5 # 限制循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools / --disallowed-tools \"read_file,git\"\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # default|plan|acceptEdits|bypassPermissions\nchainlesschain agent -p \"...\" --resume | --continue # 会话恢复 + 自动压缩\nchainlesschain agent -p \"...\" --resume --fork-session # 恢复时分叉到新会话 id,原会话不动(claude --fork-session 平价)\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时 MCP({\"mcpServers\":{...}})\nchainlesschain agent -p \"...\" --mcp-config m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 model/env + 权限覆盖\nchainlesschain agent -p \"...\" --think | --ultrathink # Anthropic 扩展思考(其它 provider 自动忽略)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"图里是什么?\" # 多模态图像输入(可重复 --image)\nchainlesschain agent -p \"...\" --json-schema schema.json # 结构化输出:最终回答必须通过 JSON Schema 校验\nchainlesschain agent -p --input-format stream-json --interactive-approvals # 危险操作改结构化审批往返(见下)\nchainlesschain agent -p \"...\" --safe-mode # 裸跑:禁项目记忆/settings hooks/记忆召回/IDE 上下文/状态行(权限规则仍生效)\nchainlesschain agent -p \"...\" --bare # --safe-mode 之上再禁 skills/插件/MCP·IDE 自动连接——脚本化调用的最小最快面(显式 --mcp-config 仍加载)\nchainlesschain agent --disable-slash-commands # 交互 REPL:'/' 开头输入原样给模型不当斜杠命令(/exit /quit 仍可用)\nchainlesschain agent --ax-screen-reader # 读屏友好输出:无 ANSI 颜色、无原地状态行重绘(也可 CC_SCREEN_READER=1)\n```\n\n**交互式审批**(`--interactive-approvals`,v0.162.45+,仅 stream-json 双工模式):confirm 级决策(危险 shell + settings/hook `ask`)从 headless 一律拒绝(fail-closed)变为**结构化往返** —— emit `approval_request`(id/tool/command/risk/rule/reason),阻塞该工具等 stdin 的 `{type:\"approval\",id,approve}`(裁决走中断同款并发泵,mid-turn 即时生效);超时(`CC_APPROVAL_TIMEOUT_MS` 默认 120s)与 stdin 关闭一律 fail-closed 并 emit `approval_resolved`。不加 flag 的 CI/管道行为字节不变。VS Code Chat 面板基于此渲染内联 Approve/Deny 审批卡。\n\n**逐回合 LLM 覆盖**(`--input-format stream-json`,v0.162.96+):stream-json 的用户事件可携带可选 `llm` 字段,**只对这一回合**切换模型而无需重启会话:\n\n```json\n{\n \"type\": \"user\",\n \"text\": \"...\",\n \"llm\": {\n \"provider\": \"ollama\",\n \"model\": \"qwen2:7b\",\n \"baseUrl\": \"...\",\n \"apiKey\": \"...\"\n }\n}\n```\n\n只覆盖给出的字段(`provider` / `model` 至少给一个,空白忽略),其余沿用会话默认——例如把某一回合从云端模型路由到你自己 PC 上的 Ollama 以提升隐私。图像回合的视觉模型切换优先级更高(图片必须用视觉模型)。IDE 聊天面板与 PDH 个人助手据此做逐回合模型路由。\n\n**结构化输出**(`--json-schema `):最终回答必须是通过该 JSON Schema(子集:\ntype/properties/required/items/enum/const/additionalProperties)校验的 JSON——回复自动提取\n(裸 / 围栏 / 嵌入三形态),校验失败带错误清单纠错重跑(共 3 次),stdout 只打印校验通过的\nJSON,耗尽返回退出码 1。与 `--output-format stream-json` 互斥。脚本消费 agent 输出不再 parse 自由文本。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`)额外 emit\n`stream_event`/`content_block_delta`;真增量覆盖 ollama + anthropic + OpenAI 兼容\n(openai/deepseek/dashscope/mistral/gemini/volcengine),含流式 tool-call 重组。\n\n**扩展思考**(`--think` / `--ultrathink` / `--thinking-budget`):Anthropic extended thinking 的\nopt-in 开关。`--think` 默认强度、`--think hard|ultra` 指定强度、`--ultrathink` = `--think ultra`。\n自适应思考模型(Opus 4.6/4.7/4.8、Sonnet 4.6)走 `output_config.effort`;legacy 模型(Sonnet 4.5、\nOpus 4.0–4.5)走 `enabled + budget_tokens`,由 `--thinking-budget` 设预算(自适应模型忽略它)。\n\n**多模态 / 图像输入**(`--image `,可重复,仅 headless):附图给视觉模型。支持\npng/jpg/jpeg/gif/webp(坏扩展名立刻报错)。内部统一成 OpenAI 形状的 `image_url`,再按 provider 转换——\nOpenAI 兼容(volcengine/doubao 等)原样透传、ollama 转 `{content, images:[base64]}`、anthropic 转\n`image` content block;纯文本运行请求形状不变。\n\n**OpenTelemetry 遥测**(`--otlp `,Phase 7):把本次 agent 运行的真实 span 以 OTLP/JSON 落盘,供真 collector 摄入——**零新增依赖**(不引 `@opentelemetry/*`)。覆盖:每次模型调用 `agent.model` span(provider/model + input/output/**cache-read/cache-write** token + has_tool_calls)、每次工具调用 `agent.tool` span(tool.name + is_error)、provider fallback 记 `agent.model.fallback` counter(按 reason)。无 `--otlp` 时零成本(`_withSpan` 短路),写文件 best-effort 不改退出码。\n\n```bash\nchainlesschain agent -p \"重构 auth 模块\" --otlp run-spans.json\n# → run-spans.json:resourceSpans(typed attr string/double/bool),可喂给 Jaeger/Tempo/collector\n```\n\n**失败自动重驱动**(`--auto-rewake` / `--max-rewakes `,Phase 6):当一个 `asyncRewake:true` 的异步 hook(如后台测试套件)在**回合结束时失败**,把结构化失败报告作为新一轮 user 输入追加,**自动重新驱动 agent 去修**。`--max-rewakes` 设每轮上限(默认 1,给出正值即隐含开启 `--auto-rewake`)。**opt-in**:默认关闭时一次性 `cc agent -p` 行为**逐字节不变**(CI/脚本不会被静默重跑);重驱动共享迭代预算防失控。\n\n```bash\n# 后台测试失败即自动修一轮(最多重驱 2 次)\nchainlesschain agent -p \"实现 X 并让测试通过\" --auto-rewake --max-rewakes 2\n```\n\n> 详见异步 Hooks(`async:true` / `asyncRewake:true`)与 SessionResume/SessionPause 事件:[Hooks 系统](./hooks.md)。\n\n### MCP — 把工具喂给 agent 循环\n\n两条路(**headless + 交互**都支持,合并进同一 client,同名 ad-hoc 优先):`--mcp-config` 临时挂载,\n或 `cc mcp add --auto-connect` 注册后每次自动连(`--no-mcp` 跳过)。工具名 `mcp____`。\n\n### Subagents — `cc agents` 子代理\n\n`.chainlesschain/agents/*.md`(原生优先)或 `.claude/agents/*.md`(Claude Code 可移植)定义命名子代理:\n**正文 = system prompt**,frontmatter `tools`(白名单)/ `model`。\n\n```bash\nchainlesschain agents list | show | run \"\" | new [--tools ...] [--claude]\n```\n\n`cc agents run` 把子代理映射到 headless 运行;主 agent 经 `spawn_sub_agent({ agent: \"review:security\" })`\n委派——加载该子代理的 system prompt + 工具白名单 + `model:`(继承父 provider/key,只覆盖 model)。\n\n### Context — `cc context` 上下文窗口占用\n\n```bash\nchainlesschain context [] [--json] [--model ] # 按角色拆 token + 占比 + 余量(/context 对标)\n```\n\n## IDE 桥接 — 自动连接编辑器 + 实时感知\n\n在 VS Code / JetBrains 集成终端里跑 `cc agent` 会**自动连接**编辑器的桥接 MCP server(保留名 `ide`,`--ide` 强制 / `--no-ide` 禁用),并获得两个自动行为:\n\n- **提交时共享选区/打开文件**:每条 prompt 自动附带 `` 块(活跃文件、打开 tab、选中代码),模型即时知道\"你正看着什么\";只进在途消息,不写会话持久化。\n- **编辑后诊断回喂**:agent 改完文件,编辑器的 error/warning 自动回流到工具结果,同一循环内自修。\n\n`CC_IDE_CONTEXT=0` 关闭自动感知(4 个 `mcp__ide__*` 工具仍可显式调用)。完整安装/协议/排错见 [IDE 桥接](/chainlesschain/ide-bridge)。\n\n## PDH 桥接 — 自动连接安卓 App 的设备数据采集\n\nIDE 桥接的**姊妹能力**,把同样的\"App-as-MCP-server\"反转模式搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\",`cc agent` 在 App 内置终端里**自动连接**(保留名 `pdh`,`--pdh` 强制 / `--no-pdh` 禁用),从而能**主动指挥设备采集自己的数据**:\n\n- **6 个设备工具**(`mcp__pdh__*`):`collect_system_data`(L2 联系人+应用,免 root/登录)、`collect_app_data`(L3,7 源:微博/B站/12306/抖音/小红书/头条/快手)、`salvage_app_data`(L4 root 内存打捞)、`collect_files`(L1 本地文件)、`list_collectors`、`pdh_ping`。\n- **引导式协同**:采集前置缺失(未登录/cookie 未暖)时工具返回 `assist_required`,agent 引导你在对应 App 内完成一步再重试(server 标 `longRunning`,免单次调用超时)。\n\n```bash\ncc pdh list / status / doctor # 检视 PDH 发现状态(token 脱敏)\n```\n\n完整安装/协议/排错见 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## 内置工具\n\n代理模式提供 25 个内置工具(cli ≥ 0.162.158):\n\n| 工具 | 说明 |\n| ------------------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `read_file` | 读取文件内容(支持 `hashed: true` 返回带哈希锚点的内容) |\n| `write_file` | 写入文件 |\n| `edit_file` | 编辑文件(查找替换) |\n| `edit_file_hashed` | 基于内容哈希锚点的行编辑(抗空白/行号漂移) |\n| `run_shell` | 执行 Shell 命令(超时 60s,输出截断 30KB;`run_in_background: true` 返回 task_id) |\n| `check_shell` | 轮询/终止后台 `run_shell` 任务(增量读取 + `kill`,详见[后台 Shell](./run-shell-background)) |\n| `git` | Git 操作封装(status / diff / log / commit 等) |\n| `search_files` | 搜索文件内容 |\n| `search_sessions` | 跨历史会话语义搜索 |\n| `list_dir` | 列出目录内容 |\n| `run_skill` | 运行内置技能 |\n| `list_skills` | 列出可用技能 |\n| `run_code` | 编写并执行代码(Python/Node.js/Bash),超时 1-300s,输出截断 50KB |\n| `spawn_sub_agent` | 分派独立上下文的子代理(支持 `profile: explorer/executor/design`) |\n| `web_fetch` | 抓取 URL 内容(默认拒绝私网 / SSRF 防护) |\n| `web_search` | 联网搜索(7 个可插拔搜索源,`auto` 按密钥择优,详见[联网搜索](./web-search)) |\n| `notebook_edit` | 编辑 Jupyter `.ipynb` 单元格:按 `cell_id`(优先)或 `cell_index` 定位,`edit_mode` = replace(默认)/ insert / delete |\n| `todo_write` | 会话级待办清单(open-agents 对标) |\n| `ask_user_question` | 主动向用户提问并阻塞等待回答 |\n| `code_intelligence` | LSP 语义代码智能:定义 / 引用 / 诊断 / 重命名(详见 [code-intel](./cli-code-intel)) |\n| `slash_command` | 在会话内执行 REPL 斜杠命令 |\n| `notify` | 落一条通知意图到 `~/.chainlesschain/agent-schedule/`,由 [cc agenda](./cli-agenda) 消费触发 |\n| `schedule` | 落一条定时任务意图(同上,agenda 调度执行) |\n| `publish_artifact` | 把完工交付物发布进个人交付物库,只回传元数据(详见 [Artifacts](./cli-artifacts)) |\n| `browser_state` | 观察用户 Chrome 当前状态(CDP,需先 `cc browse chrome launch`):URL/标题/标签页 + 观察窗口 console/失败网络请求 + DOM 快照(40k cap)+ 可选截图;只读不操控,仅 loopback |\n\n### run_code 工具详情\n\n`run_code` 是 v0.40.3 新增的代码执行工具,让 AI 能主动编写脚本解决用户问题。\n\n**参数**:\n| 参数 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `language` | string | 是 | 编程语言:`python`、`node`、`bash` |\n| `code` | string | 是 | 要执行的代码 |\n| `timeout` | number | 否 | 超时时间(秒),默认 60,最大 300 |\n\n**执行流程**:\n\n1. 代码写入临时文件(`os.tmpdir()` + 时间戳)\n2. 根据语言选择解释器(`python3`/`python`、`node`、`bash`)\n3. 执行并捕获输出(最大 50KB)\n4. 返回 `{ success, output, language, duration }` 或错误信息\n5. 清理临时文件\n\n**语言选择建议**:\n\n- **Python**: 数据处理、数学计算、Web 抓取、文件批量操作\n- **Node.js**: JSON 处理、API 调用、npm 生态相关\n- **Bash**: 系统管理、文件操作、管道命令\n\n**使用示例**:\n\n```\n> 帮我统计当前目录下每种文件类型的数量\n\n🤖 好的,我来写个脚本统计:\n[run_code: node (156 chars)]\n .js: 42\n .json: 8\n .md: 15\n .test.js: 23\n共 88 个文件\n```\n\n### notebook_edit 工具详情\n\n`notebook_edit` 补齐了 cc 此前缺失的最后一个 Claude Code 代理工具(NotebookEdit),用于编辑 Jupyter `.ipynb` 笔记本的单元格。纯函数 `editNotebookCell()` 实现:解析 → 变更 → 以 nbformat 规范(1 空格缩进 + 末尾换行)重新序列化。\n\n**参数**:\n| 参数 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `notebook_path` | string | 是 | `.ipynb` 文件路径 |\n| `cell_id` | string | 否* | 目标单元格 id(**优先**) |\n| `cell_index` | number | 否* | 0 基单元格下标(无 `cell_id` 时用) |\n| `edit_mode` | string | 否 | `replace`(默认)/ `insert` / `delete` |\n| `new_source` | string | replace/insert 必填 | 新单元格源码 |\n\n> \\*`replace` / `delete` 须给 `cell_id` 或 `cell_index` 之一定位目标。\n\n**语义**:源码以行数组存储并保留末尾 `\\n`;`replace` 一个代码单元会**清空其 outputs 与 execution_count**;`insert` 会**生成新的 cell id**(插在目标之后,或无定位时插到顶部)。完善的错误路径:坏 JSON / 无 `cells[]` / 缺定位符 / 目标不存在 / 缺参数。\n\n## 沙箱网络隔离与出口过滤代理 (Phase 1)\n\n当 `run_shell` 在**沙箱模式**下执行(Docker 或 bubblewrap 后端)时,其网络出口由一套**策略判定 + 强制过滤**的两层机制守卫——把「agent 跑的 shell 命令能访问哪些域名」从提示词约束升级为**环境强制**。\n\n### 策略判定(`sandbox-network-policy.js`)\n\n纯共享逻辑、独立可测,决定一个目标是否放行:\n\n- **`extractHost`**: 从 URL / `host:port` / IPv6 提取主机,**归一化方括号**(修 `new URL()` 对 `[::1]` 漏括号的 SSRF 坑)。\n- **`matchesDomain`**: 支持 `*`(全放行公网)/ `*.example.com`(含 apex)/ 精确匹配,防后缀欺骗。\n- **`isPrivateHost`**: 识别 loopback / `10.` / `172.16-31` / `192.168` / `169.254` / `::1` / `fc00::` / `fe80::` / 云 metadata / IPv4-mapped。\n- **`evaluateNetworkAccess`**: 优先级 `deny > 具体 allow > 私网守卫 > \"*\" allow > 默认拒`。裸 `*` 放行公网但**不含**内网/metadata;显式白名单可覆盖私网守卫(供 dev)。\n\n### 出口过滤代理(`sandbox-egress-proxy.js`)\n\n把「判定」变「强制」:一个本地转发代理,对**每个** HTTP 请求 + HTTPS `CONNECT` 隧道调 `evaluateNetworkAccess`,拒绝的 host 直接 403 / 关隧道(**上游零接触**),私网/metadata 即使 `*` allow 也拦(SSRF 防护)。沙箱进程的 `HTTP(S)_PROXY` 指向它,其 `curl` / `npm` / `pip` / `git` / `wget` / `fetch` 出口即被过滤。\n\n- **Docker 后端**: 注入 `--add-host host.docker.internal:host-gateway` + `HTTP(S)_PROXY`。\n- **bubblewrap 后端**: `--share-net` + `HTTP(S)_PROXY` 指向 `127.0.0.1`。\n- **无网络**: 不启用网络时 Docker 加 `--network none`。\n- **fail-closed**: 配了域名限制但代理起不来 → **拒绝**无限制网络访问(绝不静默放行)。\n\n### 配置\n\n```jsonc\n{\n \"sandbox\": {\n \"network\": true, // 启用网络(false → --network none 完全断网)\n \"network.allowedDomains\": [\"registry.npmjs.org\", \"*.github.com\"],\n \"network.deniedDomains\": [\"evil.example.com\"],\n },\n}\n```\n\n配了 `allowedDomains`/`deniedDomains` 且 `network:true` 时,`run_shell` 自动起代理 → 跑命令 → 拆代理。\n\n> **诚实标注(跨平台但非内核级)**: 这是**代理层**控制,Windows 可验、免 root、跨平台。忽略 `HTTP(S)_PROXY` 的工具(或直接 syscall 建连的程序)仍需 OS 级 netns 隔离(Linux bwrap netns / macOS Seatbelt / Windows AppContainer)才能真正封死——那部分属平台相关的内核硬隔离。\n\n## Auto Pip-Install (自动安装 Python 包)\n\n> v0.40.3+ 新增\n\n当 `run_code` 工具执行 Python 代码遇到 `ModuleNotFoundError` 时,会自动尝试 `pip install` 安装缺失的包,然后重新执行脚本。\n\n### 工作流程\n\n1. 执行 Python 脚本 → 捕获 `ModuleNotFoundError: No module named 'pandas'`\n2. 从错误消息中提取包名(取顶层名称:`foo.bar` → `foo`)\n3. 通过 `isValidPackageName()` 验证包名安全性(拒绝 Shell 元字符)\n4. 执行 `python3 -m pip install pandas`(超时 120s)\n5. 重新执行脚本,返回结果\n\n### 返回示例\n\n```json\n{\n \"success\": true,\n \"output\": \"DataFrame shape: (100, 5)\\n...\",\n \"language\": \"python\",\n \"duration\": \"1523ms\",\n \"autoInstalled\": [\"pandas\"],\n \"scriptPath\": \".chainlesschain/agent-scripts/2026-03-15-10-30-45-python.py\"\n}\n```\n\n### 安全防护\n\n`isValidPackageName()` 使用正则 `/^[a-zA-Z0-9_][a-zA-Z0-9._-]*$/` 验证包名:\n\n- ✅ 合法:`pandas`, `scikit-learn`, `python_dotenv`, `Flask`\n- ❌ 拒绝:`foo; rm -rf /`, `$(whoami)`, `foo|bar`, 空字符串, 超过 100 字符\n\n## Script Persistence (脚本持久化)\n\n> v0.40.3+ 新增\n\n`run_code` 工具默认将脚本保存到项目目录下,方便后续查看和复用。\n\n### 持久化模式(默认)\n\n```\n.chainlesschain/agent-scripts/\n├── 2026-03-15-10-30-45-python.py\n├── 2026-03-15-10-31-02-node.js\n└── 2026-03-15-10-32-18-bash.sh\n```\n\n- 文件名格式:`YYYY-MM-DD-HH-MM-SS-{language}.{ext}`\n- 自动创建 `.chainlesschain/agent-scripts/` 目录\n- 返回结果中 `scriptPath` 字段指向保存路径\n\n### 临时模式(`persist: false`)\n\n- 脚本写入 `os.tmpdir()`\n- 执行完毕后自动清理(`finally` 块中 `unlinkSync`)\n- 返回结果中 `scriptPath` 为 `undefined`\n\n## Error Classification (错误分类)\n\n> v0.40.3+ 新增\n\n`run_code` 执行失败时,`classifyError()` 函数将错误分为 5 种类型,每种附带 `hint` 字段提供可操作的修复建议。\n\n| errorType | 匹配模式 | hint 示例 |\n| ------------------ | --------------------------------------- | ------------------------------------------------------------ |\n| `import_error` | `ModuleNotFoundError`, `ImportError` | `Missing Python module \"pandas\". Will attempt auto-install.` |\n| `syntax_error` | `SyntaxError`, `IndentationError` | `Syntax error on line 15. Check for typos.` |\n| `timeout` | `ETIMEDOUT`, `timed out`, exitCode=null | `Script timed out. Consider increasing timeout.` |\n| `permission_error` | `EACCES`, `Permission denied` | `Permission denied. Try a different directory.` |\n| `runtime_error` | 其他所有错误 | `Runtime error near line 42. Check traceback.` |\n\n### 返回示例(错误)\n\n```json\n{\n \"error\": \"ModuleNotFoundError: No module named 'pandas'\",\n \"stderr\": \"...\",\n \"exitCode\": 1,\n \"language\": \"python\",\n \"errorType\": \"import_error\",\n \"hint\": \"Missing Python module \\\"pandas\\\". Will attempt auto-install.\",\n \"scriptPath\": \".chainlesschain/agent-scripts/2026-03-15-10-30-45-python.py\"\n}\n```\n\n## Environment Detection (环境检测)\n\n> v0.40.3+ 新增\n\n`getEnvironmentInfo()` 在进程启动时检测运行环境信息,缓存后注入到 System Prompt 中,帮助 LLM 感知宿主环境。\n\n### 检测项\n\n| 字段 | 说明 | 示例 |\n| -------- | ------------- | ---------------------------- |\n| `os` | 操作系统 | `win32`, `linux`, `darwin` |\n| `arch` | 架构 | `x64`, `arm64` |\n| `python` | Python 解释器 | `python3 (3.11.0)` 或 `null` |\n| `pip` | pip 是否可用 | `true` / `false` |\n| `node` | Node.js 版本 | `v22.12.0` |\n| `git` | Git 是否可用 | `true` / `false` |\n\n### System Prompt 注入\n\n```\n## Environment\nOS: win32 (x64)\nPython: python3 (3.11.0) + pip\nNode.js: v22.12.0\nGit: available\n```\n\n### 架构图\n\n```\nagent-core.js (中心模块)\n │\n ├── AGENT_TOOLS (19 工具定义)\n ├── getBaseSystemPrompt() ← getEnvironmentInfo()\n ├── executeTool() → executeToolInner() → _executeRunCode()\n │ ├── classifyError()\n │ ├── isValidPackageName()\n │ └── getCachedPython()\n ├── chatWithTools() → 10 LLM providers\n ├── agentLoop() → 异步生成器 (slot-filling + tool loop)\n └── formatToolArgs()\n```\n\n## Context Engineering\n\n代理模式集成了轻量级 Context Engineering 适配器(`cli-context-engineering.js`),在每次 LLM 调用前自动构建优化的上下文消息。\n\n### 6 维上下文注入\n\n| 注入器 | 数据源 | 说明 |\n| ---------------------- | ----------------------------- | -------------------------------------------------- |\n| **Instinct** | `instinct-manager.js` | 从学习到的用户偏好生成提示(编码风格、工具偏好等) |\n| **Memory** | `hierarchical-memory.js` | 召回与当前查询相关的四层记忆(working→core) |\n| **BM25 Notes** | `bm25-search.js` + `notes` 表 | 搜索笔记库中的相关内容(topK: 3, threshold: 0.5) |\n| **Task Reminder** | 内置 | 重述当前任务目标和进度,保持 AI 聚焦 |\n| **Permanent Memory** | `permanent-memory.js` | 跨会话持久记忆 + Daily Notes + MEMORY.md |\n| **Compaction Summary** | 内置 | 被压缩消息的单行摘要,支持上下文恢复 |\n\n### 稳定前缀缓存 (Stable Prefix Cache)\n\nContext Engineering 按固定顺序排列注入内容(System Prompt → Instinct → Memory → Notes → Task → Permanent Memory),确保 LLM 的 KV-Cache 前缀尽量稳定。当上下文内容不变时,LLM 可复用已缓存的计算结果,显著降低推理延迟。\n\n### System Prompt 优化\n\n- 清理时间戳 → `[DATE]`、UUID → `[UUID]`、session ID → `[SESSION]`\n- 提升 KV-Cache 命中率,减少重复计算\n\n### 错误学习\n\n- 自动记录最近 10 条错误及解决方案\n- 注入到上下文中,避免 AI 重复犯错\n\n### 智能压缩 (`/compact`)\n\n基于重要性评分的对话压缩(替代简单的 \"保留最后 4 条\"):\n\n- 始终保留 System Prompt\n- 评分维度:时间近、含 tool_calls、关联任务目标\n- 默认保留得分最高的 6 对消息\n\n### 优雅降级\n\n- 无 DB 时自动跳过所有注入,使用静态 System Prompt\n- 每个注入步骤独立 try/catch,单个失败不影响其他\n\n## 斜杠命令\n\n> **`@` 路径补全(v0.162.38+)**:交互 REPL 中输入 `@` 后按 TAB 补全文件/目录路径(目录带尾斜杠可继续下钻,自动排除 `node_modules`/`.git`,大小写不敏感);IDE 桥接已连接时,编辑器打开的标签页混入候选并排最前(workspace 内转相对路径)。IDE 列表 5s TTL 后台刷新,补全本身同步零延迟。\n>\n> **输入排队(v0.162.40+)**:agent 回合进行中继续打字不再触发并发回合 —— 多余输入按 FIFO 排队(灰色 `queued (N)` 提示),当前回合结束后自动依次执行。\n>\n> **Esc 打断(v0.162.40+)**:回合进行中按 `Esc` 立即中止当前回合(黄色 \"turn interrupted\" 提示,非报错)——已产出的部分对话保留,排队中的输入照常继续执行。\n>\n> **会话回退 `/rewind`(v0.162.40+,steering 三件套收口)**:`/rewind` 列出历史用户轮次(新→旧编号 + 预览),`/rewind ` 把对话截回该轮之前并把原文预填到输入行(改完重发);空闲时**双击 Esc** 调出同一选择器。注意这只回退**对话**,文件状态回退用 `cc checkpoint restore`。\n>\n> **恢复摘要(v0.162.40+)**:`--resume` 重建对话后,REPL 自动打印一段**离线提取式**回顾(轮次统计 + 最后提问/回复预览,零 LLM 调用)—— 秒级回想\"刚才聊到哪了\"。\n>\n> **`!` bash 直通(v0.162.41+)**:`! ` 直接跑 shell(不经 LLM),输出回灌对话上下文(`/` 块),模型下一轮可见;Windows 走 `chcp 65001` 防乱码,120s 超时 / 30K 输出截断。\n>\n> **`#` 快捷记忆(v0.162.41+)**:`# <备注>` 一键追加到 git 根的 `cc.md`(`## Notes` 段、最新在上),本会话立即生效、之后的会话经[项目记忆](./cli-init#项目记忆)自动加载。\n>\n> **`/` 命令补全(v0.162.41+)**:行首输入 `/he` 补全 REPL 命令(命令 token 输入期间生效,空格后不打扰参数)。\n\n### 基础命令\n\n| 命令 | 说明 |\n| ---------------------- | ------------------------------------------------------------------------------------------------------- |\n| `/exit` | 退出代理 |\n| `/help` | 显示所有命令帮助 |\n| `/model ` | 切换/查看模型 |\n| `/provider

` | 切换/查看提供商,支持 8 个提供商 (ollama/anthropic/openai/deepseek/dashscope/mistral/gemini/volcengine) |\n| `/clear` | 清空对话历史 |\n| `/compact` | 智能压缩对话(基于重要性评分) |\n| `/rewind [n]` | 列出/回退到历史用户轮次(双击 Esc 同效;文件回退用 `cc checkpoint`) |\n| `! ` | bash 直通:直接跑 shell,输出进对话上下文 |\n| `# ` | 快捷记忆:追加到项目 `cc.md` 的 `## Notes` |\n| `/output-style [name]` | 列出 / 切换 [输出风格](./output-styles) 人格;`none` 清除 |\n\n### Context Engineering 命令\n\n| 命令 | 说明 |\n| ---------------------- | ---------------------------- |\n| `/task ` | 设置当前任务目标 |\n| `/task clear` | 清除任务 |\n| `/session` | 显示当前 Session ID 和消息数 |\n| `/session resume ` | 加载历史会话消息 |\n| `/reindex` | 重新索引笔记到 BM25 |\n| `/stats` | 显示 Context Engine 统计信息 |\n\n### Plan Mode 命令\n\n| 命令 | 说明 |\n| --------------- | ----------------------- |\n| `/plan` | 进入规划模式 |\n| `/plan show` | 查看当前计划 |\n| `/plan approve` | 批准计划并开始执行 |\n| `/plan reject` | 拒绝计划,重新规划 |\n| `/plan exit` | 退出规划模式 |\n| `/plan execute` | 按 DAG 依赖顺序执行计划 |\n| `/plan risk` | 显示风险评估报告 |\n\n### Cowork 命令\n\n| 命令 | 说明 |\n| -------------------------- | ------------------ |\n| `/cowork debate ` | 多视角代码评审 |\n| `/cowork compare ` | A/B 方案对比 |\n| `/cowork graph` | ASCII 代码知识图谱 |\n| `/cowork decision` | 决策追踪知识库 |\n\n### 自主模式命令\n\n| 命令 | 说明 |\n| -------------- | ------------------------- |\n| `/auto ` | 提交目标,AI 自主分解执行 |\n| `/auto status` | 查看当前自主目标进度 |\n| `/auto pause` | 暂停自主执行 |\n| `/auto resume` | 恢复自主执行 |\n| `/auto cancel` | 取消当前目标 |\n| `/auto list` | 列出所有目标(含历史) |\n\n## Plan Mode\n\n代理模式内置 Plan Mode(规划模式),让 AI 在执行复杂任务前先制定计划并获得用户审批。\n\n### 规划模式工作流\n\n1. 用户输入 `/plan` 或 AI 自动判断需要规划\n2. AI 进入只读模式(仅允许 `read_file`、`search_files`、`list_dir`)\n3. AI 生成结构化计划,包含步骤、依赖和风险评估\n4. 用户审批(`/plan approve`)后 AI 开始执行\n5. 用户可随时拒绝(`/plan reject`)要求重新规划\n\n### 使用示例\n\n```\n> /plan\n\n📋 已进入规划模式。请描述您的任务。\n\n> 重构认证模块,从 session 迁移到 JWT\n\n🤖 正在分析代码库...\n[只读: read_file auth/session.js]\n[只读: search_files \"authentication\"]\n\n📋 计划:\n1. 创建 JWT 工具模块\n2. 修改登录接口返回 JWT token\n3. 添加 JWT 验证中间件\n4. 更新受保护路径\n5. 移除 session 依赖\n6. 添加单元测试\n\n> /plan approve\n✅ 开始执行计划...\n```\n\n## 自主模式 (Autonomous Agent)\n\n代理模式内置自主模式,用户只需提交高层目标,AI 自动分解为子任务并循环执行,无需手动逐步指令。\n\n### ReAct 循环\n\n自主模式采用 ReAct(Reason → Act → Observe)循环:\n\n1. **Reason**: AI 分析当前目标和已完成的子任务,决定下一步行动\n2. **Act**: 调用工具(读写文件、执行命令、搜索等)执行子任务\n3. **Observe**: 观察工具输出,评估结果是否符合预期\n4. **重复**: 回到 Reason,直到目标完成或达到 Token 预算上限\n\n### 核心特性\n\n- **目标分解**: 自动将高层目标拆分为可执行的子任务序列\n- **自我纠错**: 工具调用失败时自动分析原因并尝试替代方案\n- **Token 预算**: 可配置的 Token 消耗上限,防止无限循环\n- **进度追踪**: `/auto status` 实时查看已完成/剩余子任务\n- **暂停/恢复**: `/auto pause` 和 `/auto resume` 控制执行节奏\n\n### 使用示例\n\n```\n> /auto 为项目添加完整的 CI/CD 配置(GitHub Actions + Docker)\n\n🤖 目标已接收,正在分解...\n 子任务 1: 分析项目结构和依赖\n 子任务 2: 创建 .github/workflows/ci.yml\n 子任务 3: 创建 Dockerfile\n 子任务 4: 创建 docker-compose.yml\n 子任务 5: 添加测试和构建步骤\n 子任务 6: 验证配置正确性\n\n[Reason] 开始分析项目结构...\n[Act] read_file: package.json\n[Observe] Node.js 项目,使用 Vitest 测试...\n[Reason] 需要创建 CI 工作流...\n[Act] write_file: .github/workflows/ci.yml\n...\n\n✅ 目标完成!已创建 4 个文件,所有配置已验证。\n\n> /auto status\n目标: 为项目添加完整的 CI/CD 配置\n状态: ✅ 已完成 (6/6 子任务)\nToken 消耗: 12,450 / 50,000\n```\n\n## SlotFiller 意图检测\n\n> v0.41.1 新增\n\nAgent 模式集成了 SlotFiller 参数槽填充引擎,在调用 LLM 之前自动检测用户消息中的已知意图,补全缺失的必需参数。\n\n### 支持的意图类型\n\n| 意图类型 | 触发关键词 | 必需参数 |\n| ------------- | --------------------------------- | -------------- |\n| `create_file` | create a file, new file, scaffold | fileType, path |\n| `deploy` | deploy, ship it, push to prod | platform |\n| `refactor` | refactor, restructure | target |\n| `test` | write tests, add test, unit test | target |\n| `analyze` | analyze, audit, review code | — |\n| `search` | search for, find all, grep | query |\n| `install` | install, add package | package |\n| `generate` | generate, create component | — |\n| `edit_file` | edit file, modify file | target |\n\n### 工作流程\n\n1. 用户输入消息(如 \"deploy this\")\n2. `CLISlotFiller.detectIntent()` 检测到 `deploy` 意图\n3. 检查必需参数 `platform` 缺失\n4. 通过 InteractionAdapter 向用户提问 \"Which platform?\"\n5. 用户回答 \"docker\"\n6. 补全结果注入到用户消息末尾:`[Context — user provided: platform: docker]`\n7. 带有完整上下文的消息发送给 LLM\n\n### 使用示例\n\n```\n> deploy this\n\n? Which platform do you want to deploy to?\n > docker\n > vercel\n > aws\n > heroku\n\n[SlotFiller] platform = \"docker\"\n\n🤖 好的,我来帮你部署到 Docker...\n[run_shell: docker build -t myapp .]\n```\n\n未识别的意图(如 \"help me understand this code\")不会触发 SlotFiller,直接传给 LLM 处理。\n\n## 使用示例\n\n### 基础使用\n\n```\n> 帮我重构 src/utils/helper.js,提取公共方法\n\n🤖 我来分析这个文件...\n[调用 read_file: src/utils/helper.js]\n[调用 edit_file: 提取公共方法到 src/utils/common.js]\n[调用 run_skill: code-review 检查重构质量]\n\n已完成重构,提取了 3 个公共方法到 common.js。\n```\n\n### 使用火山引擎(豆包)Agent 模式\n\n```bash\n# 设置环境变量\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\n\n# 启动 Agent(默认旗舰模型 doubao-seed-1-6-251015)\nchainlesschain agent --provider volcengine\n\n# 指定代码模型\nchainlesschain agent --provider volcengine --model doubao-seed-1-6-251015\n```\n\nAgent 模式下会自动根据任务类型智能选择最佳模型:\n\n```\nyou> 写一个 Python 快速排序函数\n[auto] 代码任务 → doubao-seed-1-6-251015\nai> def quicksort(arr): ...\n\nyou> 分析 TCP 三次握手的过程\n[auto] 复杂推理 → doubao-seed-1-6-251015\nai> TCP三次握手过程如下:...\n\nyou> 今天天气怎么样?\nai> (使用默认模型,快速响应)\n```\n\n### 使用 OpenAI / DeepSeek Agent 模式\n\n```bash\n# OpenAI\nexport OPENAI_API_KEY=sk-xxxxxxxxxxxx\nchainlesschain agent --provider openai --model gpt-4o\n\n# DeepSeek\nexport DEEPSEEK_API_KEY=sk-xxxxxxxxxxxx\nchainlesschain agent --provider deepseek --model deepseek-chat\n```\n\n### 通过中转站使用 Agent 模式(无需科学上网)\n\n```bash\n# 通过 API2D 中转站使用 GPT-4o\nchainlesschain agent \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n\n# 通过 One-API 网关\nchainlesschain agent \\\n --provider openai \\\n --base-url http://localhost:3000/v1 \\\n --api-key sk-oneapi-xxxxxx \\\n --model gpt-4o\n\n# 火山引擎通过代理\nchainlesschain agent \\\n --provider volcengine \\\n --base-url https://your-proxy.com/volcengine/v3 \\\n --model doubao-seed-1-6-251015\n\n# 企业 Nginx 反向代理\nchainlesschain agent \\\n --provider openai \\\n --base-url https://ai-gateway.company.com/v1 \\\n --model gpt-4o\n```\n\n> **提示**: `--base-url` 覆盖提供商默认地址,但保留 OpenAI 兼容的 API 格式。所有支持 `/chat/completions` 接口的中转站和代理均可使用。\n\n### Context Engineering 使用\n\n```\n> /task 重构认证模块\n\n✓ Task set: 重构认证模块\n\n> 查看当前进度\n\n🤖 当前任务: 重构认证模块\n[Context Engine 自动注入任务提醒 + 相关记忆 + 用户偏好]\n...\n\n> /stats\n\nDB connected: true\nNotes indexed: 42\nError history: 0\nActive task: true\n```\n\n### 会话恢复\n\n```bash\n# 第一次会话\nchainlesschain agent\n> 帮我写一个 API 服务\n> /exit\n\n# 恢复上次会话\nchainlesschain agent --session session-17...\nResumed session session-17... (8 messages)\n> 继续上次的工作\n```\n\n## 关键文件\n\n- `packages/cli/src/commands/agent.js` — 命令入口(含 `--session` 选项)\n- `packages/cli/src/repl/agent-repl.js` — 代理 REPL(9 工具 + 146 技能 + Plan Mode + Context Engineering)\n- `packages/cli/src/lib/cli-context-engineering.js` — Context Engineering 适配器(6 维注入 + 智能压缩)\n- `packages/cli/src/lib/plan-mode.js` — Plan Mode 实现\n- `packages/cli/src/lib/instinct-manager.js` — Instinct 学习引擎\n- `packages/cli/src/lib/hierarchical-memory.js` — 四层记忆系统\n- `packages/cli/src/lib/bm25-search.js` — BM25 搜索引擎\n- `packages/cli/src/lib/session-manager.js` — 会话持久化\n- `packages/cli/src/lib/permanent-memory.js` — 跨会话持久记忆(Daily Notes + MEMORY.md + BM25 混合搜索)\n- `packages/cli/src/lib/autonomous-agent.js` — ReAct 自主任务循环\n- `packages/cli/src/lib/content-recommender.js` — TF-IDF 工具相似度 + 工具链推荐\n- `packages/cli/src/lib/hook-manager.js` — Hook 管道(PreToolUse/PostToolUse/ToolError)\n- `packages/cli/src/lib/slot-filler.js` — SlotFiller 参数槽填充(意图检测 + 交互式补全)\n- `packages/cli/src/lib/agent-core.js` — Agent 核心业务逻辑(agentLoop 异步生成器)\n- `packages/cli/src/runtime/bootstrap.js` — 7 阶段无头启动\n\n## 安全考虑\n\n- `run_shell` 工具可执行任意 Shell 命令,请在可信环境中使用\n- `run_code` 工具可执行 Python/Node.js/Bash 脚本,代码在临时文件中执行并自动清理\n- `write_file` / `edit_file` 可修改文件系统,建议在 Git 仓库中使用以便回滚\n- Plan Mode 默认只读,需用户审批后才执行写操作\n- API Key 仅存储在本地,不会通过工具调用泄露\n- Context Engineering 注入的记忆/笔记仅在本地处理,不泄露到外部\n\n## 配置参考\n\n### Agent REPL 选项\n\n```javascript\n// packages/cli/src/repl/agent-repl.js\n{\n provider: \"ollama\", // ollama | anthropic | openai | deepseek |\n // dashscope | mistral | gemini | volcengine\n model: \"qwen2.5:7b\", // 模型名称\n baseUrl: \"http://localhost:11434\", // API 基础 URL\n apiKey: null, // API 密钥(云端提供商必填)\n sessionId: null, // 恢复指定会话 ID\n noRecallMemory: false, // 禁用启动时记忆召回\n recallLimit: 5, // 启动时注入的记忆条数上限\n noStream: false, // 禁用流式输出渲染\n noParkOnExit: false, // 退出时关闭而非 park 会话\n agentId: null, // 限定记忆召回范围的 agent ID\n}\n```\n\n### Context Engineering 选项\n\n```javascript\n// packages/cli/src/lib/cli-context-engineering.js\n{\n enableInstinct: true, // 注入用户偏好(Instinct)\n enableMemory: true, // 注入层次化记忆(top-5 相关记忆)\n enableBm25Notes: true, // 注入 BM25 笔记搜索结果\n bm25TopK: 3, // BM25 返回结果数\n bm25Threshold: 0.5, // BM25 相关性阈值\n enableTaskReminder: true, // 注入任务目标提醒\n enablePermanentMemory: true, // 注入跨会话持久记忆\n enableCompactionSummary: true, // 注入压缩摘要\n compactKeepCount: 6, // 智能压缩保留的消息对数\n}\n```\n\n### Hook 管道选项\n\n```javascript\n// packages/cli/src/lib/hook-manager.js\n// 通过 chainlesschain hook add 注册\n{\n event: \"PreToolUse\", // PreToolUse | PostToolUse | ToolError |\n // SessionStart | UserPromptSubmit | SessionEnd\n command: \"node my-hook.js\", // 钩子命令\n priority: 0, // 执行优先级(数值越小越先执行)\n timeout: 5000, // 超时时间(ms)\n}\n```\n\n### Plan Mode 选项\n\n```javascript\n// packages/cli/src/lib/plan-mode.js\n{\n autoEnter: false, // 是否在复杂任务时自动进入规划模式\n readOnlyTools: [ // 规划阶段允许使用的只读工具\n \"read_file\", \"search_files\", \"list_dir\"\n ],\n requireApproval: true, // 执行前必须用户审批\n enableDagExecution: true, // 按 DAG 依赖拓扑顺序执行步骤\n enableRiskAssessment: true, // 执行前显示风险评估报告\n}\n```\n\n### 自主模式选项\n\n```javascript\n// packages/cli/src/lib/autonomous-agent.js\n{\n tokenBudget: 50000, // 单目标最大 token 消耗\n maxSubtasks: 20, // 子任务数量上限\n autoRetry: true, // 工具失败时自动重试\n retryLimit: 3, // 单工具最大重试次数\n}\n```\n\n## 性能指标\n\n### 启动与初始化\n\n| 指标 | 数值 | 说明 |\n| -------------------------------- | ------ | -------------------------------------- |\n| 7 阶段无头启动(`bootstrap.js`) | ~200ms | DB 初始化 + 配置加载 + 技能注册 |\n| Context Engineering 首次初始化 | ~50ms | BM25 索引加载 + 记忆模型预热 |\n| 环境检测(`getEnvironmentInfo`) | ~30ms | 检测 Python/Node/Git,结果缓存 |\n| 138 个技能元数据加载(stub) | ~15ms | 仅加载 YAML frontmatter,body 按需读取 |\n\n### 工具执行性能\n\n| 工具 | 典型延迟 | 备注 |\n| ----------------------------- | ---------- | ----------------------- |\n| `read_file` | < 5ms | 本地文件读取 |\n| `write_file` / `edit_file` | < 10ms | 含 fsync |\n| `edit_file_hashed` | < 5ms | 哈希锚定,无需全文扫描 |\n| `search_files` | 10–200ms | 取决于代码库大小 |\n| `run_shell` | 视命令而定 | 超时 60s,输出截断 30KB |\n| `run_code` (Python/Node) | 500ms–5s | 含解释器启动开销 |\n| `run_code` + auto pip-install | +5–15s | 含 `pip install` 耗时 |\n| `web_fetch` | 500ms–3s | 含 DNS + TCP + 页面抓取 |\n| `spawn_sub_agent` | 200ms+ | 独立上下文初始化 |\n\n### Context Engineering 注入开销\n\n| 注入器 | 典型延迟 | 说明 |\n| -------------------- | ----------- | ---------------------------- |\n| Instinct | < 2ms | 内存读取,已缓存 |\n| Memory(层次化记忆) | 5–20ms | BM25 + 向量相关性搜索 |\n| BM25 Notes | 10–30ms | topK=3,threshold=0.5 |\n| Task Reminder | < 1ms | 内存字段拼接 |\n| Permanent Memory | 5–15ms | MEMORY.md + Daily Notes 读取 |\n| Compaction Summary | < 1ms | 内存字段拼接 |\n| **6 维合计** | **20–70ms** | KV-Cache 稳定前缀可复用 |\n\n### 智能压缩效果\n\n| 指标 | 数值 |\n| -------------------- | ----------- |\n| 压缩率 | 0.76–0.93 |\n| 节省 token | 7–24% |\n| 压缩触发阈值 | 消息数 ≥ 12 |\n| 保留消息对数(默认) | 6 对 |\n\n## 测试覆盖率\n\n### 单元测试\n\n```\n✅ agent-core.test.js - 66 测试 (agentLoop / executeTool / chatWithTools)\n✅ hashline.test.js - 29 测试 (hashLine / annotateLines / findByHash / replaceByHash)\n✅ agent-core-edit-hashed.test.js - 12 测试 (edit_file_hashed handler)\n✅ slot-filler.test.js - 18 测试 (intent detection / slot filling / interaction)\n✅ plan-mode.test.js - 22 测试 (DAG 执行 / 风险评估 / 审批流)\n✅ autonomous-agent.test.js - 20 测试 (ReAct 循环 / 目标分解 / 自动重试)\n✅ context-engineering.test.js - 24 测试 (6 维注入 / 智能压缩 / 优雅降级)\n✅ session-hooks.test.js - 15 测试 (SessionStart / UserPromptSubmit / SessionEnd)\n✅ skill-mcp.test.js - 26 测试 (SKILL.md inline MCP / mount / unmount)\n```\n\n### 集成测试\n\n```\n✅ agent-repl-integration.test.js - 18 测试 (完整 REPL 生命周期 + 会话恢复)\n✅ run-code-integration.test.js - 16 测试 (Python / Node / Bash 执行 + auto pip-install)\n✅ context-engineering-e2e.test.js - 12 测试 (6 维注入 × 有 DB / 无 DB 场景)\n```\n\n### 验证结果(v5.0.2.10)\n\n| 验证项 | 结果 |\n| ----------------------- | ---------- |\n| `agent-core` 定向单测 | ✅ 66/66 |\n| `hashline` 单测 | ✅ 29/29 |\n| `edit_file_hashed` 单测 | ✅ 12/12 |\n| `slot-filler` 单测 | ✅ 18/18 |\n| `plan-mode` 单测 | ✅ 22/22 |\n| `session-hooks` 单测 | ✅ 15/15 |\n| `skill-mcp` 单测 | ✅ 26/26 |\n| Agent REPL 集成 | ✅ 18/18 |\n| `run-code` 集成 | ✅ 16/16 |\n| CLI 本轮定向合计 | ✅ 222/222 |\n\n## 故障排查\n\n| 问题 | 解决方案 |\n| -------------------- | ----------------------------------------------------- |\n| 工具调用失败 | 检查当前目录的文件权限 |\n| Plan Mode 不生效 | 输入 `/plan` 手动进入规划模式 |\n| 代理响应过慢 | 切换到更快的模型(如 `qwen2.5:7b`)或使用云端 API |\n| `run_shell` 权限不足 | 检查当前用户的 Shell 执行权限 |\n| Context 注入无效果 | 检查 DB 是否初始化(`chainlesschain db init`) |\n| `--session` 恢复失败 | 确认 Session ID 存在(`chainlesschain session list`) |\n| 记忆/笔记未注入 | 使用 `/stats` 检查 DB 连接和索引状态 |\n| `/auto` 无响应 | 检查 LLM 连接和 Token 预算 |\n| Hook 执行失败 | 使用 `chainlesschain hook stats` 检查钩子状态 |\n\n## 相关文档\n\n- [LLM 中转站与自定义接入](./cli-llm-proxy) — 中转站、代理、自建网关完整指南\n- [技能系统](./cli-skill) — 146 个内置技能\n- [Plan Mode](./plan-mode) — 桌面端 Plan Mode 详情\n- [AI 对话](./cli-chat) — 基础对话功能\n- [会话管理](./cli-session) — 代理会话持久化\n- [层次化记忆](./cli-hmemory) — 四层记忆系统\n- [Instinct 学习](./cli-instinct) — 用户偏好学习\n- [IDE 桥接](./ide-bridge) — 自动连接编辑器的 MCP server(`--ide` / `mcp__ide__*`)\n- [PDH Bridge 个人数据 IDE 桥接](./pdh-bridge) — 自动连接安卓 App 的设备采集(`--pdh` / `mcp__pdh__*`)\n"}, "4db48500206ff6a789c53c8197933113b06a6097": {"t": 1785809910.18121, "n": 41992, "h": "29c5f47d6ce20f1a", "c": "# 代理模式 (agent)\n\n> Headless 命令 — 不依赖桌面 GUI,直接使用核心包运行。适用于服务器、CI/CD、容器化等无桌面环境。\n\n## 核心特性\n\n- 🤖 **Claude Code 风格**: 代理式 AI 会话,自主完成任务\n- 🔧 **25 个内置工具**: 读写/编辑文件、执行命令(含后台)、搜索代码库与会话、代码执行、联网搜索/抓取、Jupyter 编辑、子代理、技能调用、LSP 代码智能、通知/定时意图、交付物发布、浏览器状态观察等\n- 🎯 **146 个技能**: 集成全部内置技能\n- 📋 **Plan Mode**: AI 制定计划,用户审批后执行\n- 💾 **会话持久化**: 自动保存,支持 `--session` 断点恢复\n- 🧠 **Context Engineering**: 6 维上下文注入(Instinct / Memory / BM25 Notes / Task / Permanent Memory / Compaction Summary)\n- 🗜️ **智能压缩**: 基于重要性评分的对话压缩(替代硬编码截断)\n- 🔗 **Hook 管道**: PreToolUse/PostToolUse/ToolError 钩子集成\n- 🤖 **自主模式**: /auto 命令提交目标,AI 自动分解执行\n- ⚠️ **DAG 执行 + 风险评估**: /plan execute 按依赖拓扑排序执行,/plan risk 显示风险评分\n- 🎯 **SlotFiller 意图检测**: 自动检测用户消息中的缺失参数,交互式补全后再调用 LLM\n- 🎭 **Persona 系统**: 项目级 AI 角色配置,自动替换默认编码助手,支持工具权限控制\n- 🎨 **输出风格**: 命名人格叠加系统提示(`--output-style`),保留编码能力改行为/语气,详见 [输出风格](./output-styles)\n\n## 系统架构\n\n```\nagent 命令 → agent.js (Commander) → agent-repl.js\n │\n ┌──────────┬──────────┬─────────────┼──────────────┬─────────────┬──────────┐\n ▼ ▼ ▼ ▼ ▼ ▼ ▼\n 工具系统 Plan Mode Hook管道 Context Engine 会话管理 Autonomous Bootstrap\n (19 tools) (plan-mode) (hook- (cli-context- (session- Agent (bootstrap.js)\n manager) engineering.js) manager) (ReAct) │\n │ │ │ │ │ │ 7-stage init\n ▼ ▼ ▼ ▼ ▼ ▼ (DB/Config)\n read/write 只读→计划→ PreToolUse 6维注入: 自动保存 /auto\n edit/shell 审批→执行 PostToolUse Instinct 到SQLite 目标分解\n search/code DAG执行 ToolError Memory 断点恢复 ReAct循环\n skill/list 风险评估 BM25 Notes 自动纠错\n Task重述\n Permanent Memory\n Compaction Summary\n Multi-Provider (10 LLM)\n Task Model Selector\n```\n\n## 概述\n\n启动 Claude Code 风格的代理式 AI 会话。AI 可读写文件、执行命令、搜索代码库、运行代码脚本(Python/Node.js/Bash)、调用 146 个内置技能。\n支持 8 个 LLM 提供商(ollama/anthropic/openai/deepseek/dashscope/mistral/gemini/volcengine)和自主模式(/auto)。Agent 模式下自动根据任务类型智能选择最佳模型。\n通过 6 维 Context Engineering 自动注入用户偏好(Instinct)、相关记忆(Hierarchical Memory)、相关笔记(BM25 搜索)、任务目标提醒、跨会话持久记忆(Permanent Memory)和压缩摘要(Compaction Summary),使 AI 保持上下文聚焦。\n\n**Persona 支持**: 当项目配置了 Persona(通过 `init --template medical-triage` 或 `persona set`),Agent 自动使用自定义角色替代默认编码助手,并按 `toolsDisabled` 过滤可用工具。详见 [Persona 命令](./cli-persona)。\n\n## 命令参考\n\n```bash\nchainlesschain agent # 默认读 ~/.chainlesschain/config.json 的 llm 配置;无配置时 Ollama qwen2.5:7b\nchainlesschain a --model llama3 # 短别名\nchainlesschain agent --provider openai --api-key sk-...\nchainlesschain agent --session # 恢复历史会话\n```\n\n> `agent` 命令的短别名为 `a`,两者完全等价。\n>\n> **配置默认(v0.162.43+)**:不带 `--provider/--model` 的裸 `cc agent` 会读取 `config.json` 的 `llm` 段作为默认(provider/model/apiKey/baseUrl),与 `cc chat`/桌面端一致;显式 flag 仍然优先。\n\n### 命令选项\n\n| 选项 | 说明 | 默认值 |\n| ----------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------ |\n| `--model ` | LLM 模型名称 | `qwen2.5:7b` |\n| `--provider ` | LLM 提供商(ollama/anthropic/openai/deepseek/dashscope/mistral/gemini/volcengine) | `ollama` |\n| `--base-url ` | API 基础 URL | `http://localhost:11434` |\n| `--api-key ` | API 密钥 | — |\n| `--session ` | 恢复历史会话 | — |\n| `--add-dir

` | 额外工作根(可重复) | — |\n| `--mcp-config ` | 临时挂载 MCP server,工具 → `mcp____` | — |\n| `--no-mcp` | 不自动连 `cc mcp add --auto-connect` 的服务 | (默认连) |\n| `--settings ` | 一次性 `.claude/settings.json`:权限规则 + `model`/`env` 覆盖 | — |\n| `--output-style ` | 套用命名人格(内置 `explanatory`/`learning` 或 `.claude/output-styles/.md`),详见 [输出风格](./output-styles) | — |\n\n## Headless 模式 (`agent -p`) 与 Claude Code 平价旗标\n\n`cc agent -p` 是**单轮非交互**执行(`claude -p` 对标),进 CI / 管道 / 脚本;prompt 来自\n`-p` 值 / 位置参数 / stdin。完整旗标 + 输出格式 + 权限档表见\n[CLI_COMMANDS_REFERENCE → managed-agents](https://github.com/chainlesschain/chainlesschain/blob/main/docs/cli/managed-agents.md)。\n\n```bash\nchainlesschain agent -p \"fix the bug in x.js\" # 单轮执行后退出\nchainlesschain agent -p \"...\" --output-format json|stream-json # 机器可读 / 逐事件 NDJSON\nchainlesschain agent -p \"...\" --max-turns 5 # 限制循环迭代上限\nchainlesschain agent -p \"...\" --allowed-tools / --disallowed-tools \"read_file,git\"\nchainlesschain agent -p \"...\" --permission-mode bypassPermissions # default|plan|acceptEdits|bypassPermissions\nchainlesschain agent -p \"...\" --resume | --continue # 会话恢复 + 自动压缩\nchainlesschain agent -p \"...\" --resume --fork-session # 恢复时分叉到新会话 id,原会话不动(claude --fork-session 平价)\nchainlesschain agent -p \"...\" --output-format stream-json --include-partial-messages # 逐 token 增量\nchainlesschain agent -p \"...\" --input-format stream-json # 持续从 stdin 喂多轮 NDJSON 用户事件\nchainlesschain agent -p \"...\" --mcp-config ./mcp.json # 临时 MCP({\"mcpServers\":{...}})\nchainlesschain agent -p \"...\" --mcp-config m.json --permission-prompt-tool mcp__auth__approve # 审批交 MCP 工具\nchainlesschain agent -p \"...\" --settings ./run.json # 一次性 model/env + 权限覆盖\nchainlesschain agent -p \"...\" --think | --ultrathink # Anthropic 扩展思考(其它 provider 自动忽略)\nchainlesschain agent -p \"...\" --think --thinking-budget 8000 # legacy Claude 的思考 token 预算\nchainlesschain agent --image shot.png -p \"图里是什么?\" # 多模态图像输入(可重复 --image)\nchainlesschain agent -p \"...\" --json-schema schema.json # 结构化输出:最终回答必须通过 JSON Schema 校验\nchainlesschain agent -p --input-format stream-json --interactive-approvals # 危险操作改结构化审批往返(见下)\nchainlesschain agent -p \"...\" --safe-mode # 裸跑:禁项目记忆/settings hooks/记忆召回/IDE 上下文/状态行(权限规则仍生效)\nchainlesschain agent -p \"...\" --bare # --safe-mode 之上再禁 skills/插件/MCP·IDE 自动连接——脚本化调用的最小最快面(显式 --mcp-config 仍加载)\nchainlesschain agent --disable-slash-commands # 交互 REPL:'/' 开头输入原样给模型不当斜杠命令(/exit /quit 仍可用)\nchainlesschain agent --ax-screen-reader # 读屏友好输出:无 ANSI 颜色、无原地状态行重绘(也可 CC_SCREEN_READER=1)\n```\n\n**交互式审批**(`--interactive-approvals`,v0.162.45+,仅 stream-json 双工模式):confirm 级决策(危险 shell + settings/hook `ask`)从 headless 一律拒绝(fail-closed)变为**结构化往返** —— emit `approval_request`(id/tool/command/risk/rule/reason),阻塞该工具等 stdin 的 `{type:\"approval\",id,approve}`(裁决走中断同款并发泵,mid-turn 即时生效);超时(`CC_APPROVAL_TIMEOUT_MS` 默认 120s)与 stdin 关闭一律 fail-closed 并 emit `approval_resolved`。不加 flag 的 CI/管道行为字节不变。VS Code Chat 面板基于此渲染内联 Approve/Deny 审批卡。\n\n**逐回合 LLM 覆盖**(`--input-format stream-json`,v0.162.96+):stream-json 的用户事件可携带可选 `llm` 字段,**只对这一回合**切换模型而无需重启会话:\n\n```json\n{\n \"type\": \"user\",\n \"text\": \"...\",\n \"llm\": {\n \"provider\": \"ollama\",\n \"model\": \"qwen2:7b\",\n \"baseUrl\": \"...\",\n \"apiKey\": \"...\"\n }\n}\n```\n\n只覆盖给出的字段(`provider` / `model` 至少给一个,空白忽略),其余沿用会话默认——例如把某一回合从云端模型路由到你自己 PC 上的 Ollama 以提升隐私。图像回合的视觉模型切换优先级更高(图片必须用视觉模型)。IDE 聊天面板与 PDH 个人助手据此做逐回合模型路由。\n\n**结构化输出**(`--json-schema `):最终回答必须是通过该 JSON Schema(子集:\ntype/properties/required/items/enum/const/additionalProperties)校验的 JSON——回复自动提取\n(裸 / 围栏 / 嵌入三形态),校验失败带错误清单纠错重跑(共 3 次),stdout 只打印校验通过的\nJSON,耗尽返回退出码 1。与 `--output-format stream-json` 互斥。脚本消费 agent 输出不再 parse 自由文本。\n\n**逐 token 增量**(`--include-partial-messages`,仅 `--output-format stream-json`)额外 emit\n`stream_event`/`content_block_delta`;真增量覆盖 ollama + anthropic + OpenAI 兼容\n(openai/deepseek/dashscope/mistral/gemini/volcengine),含流式 tool-call 重组。\n\n**扩展思考**(`--think` / `--ultrathink` / `--thinking-budget`):Anthropic extended thinking 的\nopt-in 开关。`--think` 默认强度、`--think hard|ultra` 指定强度、`--ultrathink` = `--think ultra`。\n自适应思考模型(Opus 4.6/4.7/4.8、Sonnet 4.6)走 `output_config.effort`;legacy 模型(Sonnet 4.5、\nOpus 4.0–4.5)走 `enabled + budget_tokens`,由 `--thinking-budget` 设预算(自适应模型忽略它)。\n\n**多模态 / 图像输入**(`--image `,可重复,仅 headless):附图给视觉模型。支持\npng/jpg/jpeg/gif/webp(坏扩展名立刻报错)。内部统一成 OpenAI 形状的 `image_url`,再按 provider 转换——\nOpenAI 兼容(volcengine/doubao 等)原样透传、ollama 转 `{content, images:[base64]}`、anthropic 转\n`image` content block;纯文本运行请求形状不变。\n\n**OpenTelemetry 遥测**(`--otlp `,Phase 7):把本次 agent 运行的真实 span 以 OTLP/JSON 落盘,供真 collector 摄入——**零新增依赖**(不引 `@opentelemetry/*`)。覆盖:每次模型调用 `agent.model` span(provider/model + input/output/**cache-read/cache-write** token + has_tool_calls)、每次工具调用 `agent.tool` span(tool.name + is_error)、provider fallback 记 `agent.model.fallback` counter(按 reason)。无 `--otlp` 时零成本(`_withSpan` 短路),写文件 best-effort 不改退出码。\n\n```bash\nchainlesschain agent -p \"重构 auth 模块\" --otlp run-spans.json\n# → run-spans.json:resourceSpans(typed attr string/double/bool),可喂给 Jaeger/Tempo/collector\n```\n\n**失败自动重驱动**(`--auto-rewake` / `--max-rewakes `,Phase 6):当一个 `asyncRewake:true` 的异步 hook(如后台测试套件)在**回合结束时失败**,把结构化失败报告作为新一轮 user 输入追加,**自动重新驱动 agent 去修**。`--max-rewakes` 设每轮上限(默认 1,给出正值即隐含开启 `--auto-rewake`)。**opt-in**:默认关闭时一次性 `cc agent -p` 行为**逐字节不变**(CI/脚本不会被静默重跑);重驱动共享迭代预算防失控。\n\n```bash\n# 后台测试失败即自动修一轮(最多重驱 2 次)\nchainlesschain agent -p \"实现 X 并让测试通过\" --auto-rewake --max-rewakes 2\n```\n\n> 详见异步 Hooks(`async:true` / `asyncRewake:true`)与 SessionResume/SessionPause 事件:[Hooks 系统](./hooks.md)。\n\n### MCP — 把工具喂给 agent 循环\n\n两条路(**headless + 交互**都支持,合并进同一 client,同名 ad-hoc 优先):`--mcp-config` 临时挂载,\n或 `cc mcp add --auto-connect` 注册后每次自动连(`--no-mcp` 跳过)。工具名 `mcp____`。\n\n### Subagents — `cc agents` 子代理\n\n`.chainlesschain/agents/*.md`(原生优先)或 `.claude/agents/*.md`(Claude Code 可移植)定义命名子代理:\n**正文 = system prompt**,frontmatter `tools`(白名单)/ `model`。\n\n```bash\nchainlesschain agents list | show | run \"\" | new [--tools ...] [--claude]\n```\n\n`cc agents run` 把子代理映射到 headless 运行;主 agent 经 `spawn_sub_agent({ agent: \"review:security\" })`\n委派——加载该子代理的 system prompt + 工具白名单 + `model:`(继承父 provider/key,只覆盖 model)。\n\n### Context — `cc context` 上下文窗口占用\n\n```bash\nchainlesschain context [] [--json] [--model ] # 按角色拆 token + 占比 + 余量(/context 对标)\n```\n\n## IDE 桥接 — 自动连接编辑器 + 实时感知\n\n在 VS Code / JetBrains 集成终端里跑 `cc agent` 会**自动连接**编辑器的桥接 MCP server(保留名 `ide`,`--ide` 强制 / `--no-ide` 禁用),并获得两个自动行为:\n\n- **提交时共享选区/打开文件**:每条 prompt 自动附带 `` 块(活跃文件、打开 tab、选中代码),模型即时知道\"你正看着什么\";只进在途消息,不写会话持久化。\n- **编辑后诊断回喂**:agent 改完文件,编辑器的 error/warning 自动回流到工具结果,同一循环内自修。\n\n`CC_IDE_CONTEXT=0` 关闭自动感知(4 个 `mcp__ide__*` 工具仍可显式调用)。完整安装/协议/排错见 [IDE 桥接](/chainlesschain/ide-bridge)。\n\n## PDH 桥接 — 自动连接安卓 App 的设备数据采集\n\nIDE 桥接的**姊妹能力**,把同样的\"App-as-MCP-server\"反转模式搬到**手机**:安卓 App 在本机跑一个\"设备能力 MCP server\",`cc agent` 在 App 内置终端里**自动连接**(保留名 `pdh`,`--pdh` 强制 / `--no-pdh` 禁用),从而能**主动指挥设备采集自己的数据**:\n\n- **6 个设备工具**(`mcp__pdh__*`):`collect_system_data`(L2 联系人+应用,免 root/登录)、`collect_app_data`(L3,7 源:微博/B站/12306/抖音/小红书/头条/快手)、`salvage_app_data`(L4 root 内存打捞)、`collect_files`(L1 本地文件)、`list_collectors`、`pdh_ping`。\n- **引导式协同**:采集前置缺失(未登录/cookie 未暖)时工具返回 `assist_required`,agent 引导你在对应 App 内完成一步再重试(server 标 `longRunning`,免单次调用超时)。\n\n```bash\ncc pdh list / status / doctor # 检视 PDH 发现状态(token 脱敏)\n```\n\n完整安装/协议/排错见 [PDH Bridge 个人数据 IDE 桥接](/chainlesschain/pdh-bridge)。\n\n## 内置工具\n\n代理模式提供 25 个内置工具(cli ≥ 0.162.158):\n\n| 工具 | 说明 |\n| ------------------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `read_file` | 读取文件内容(支持 `hashed: true` 返回带哈希锚点的内容) |\n| `write_file` | 写入文件 |\n| `edit_file` | 编辑文件(查找替换) |\n| `edit_file_hashed` | 基于内容哈希锚点的行编辑(抗空白/行号漂移) |\n| `run_shell` | 执行 Shell 命令(超时 60s,输出截断 30KB;`run_in_background: true` 返回 task_id) |\n| `check_shell` | 轮询/终止后台 `run_shell` 任务(增量读取 + `kill`,详见[后台 Shell](./run-shell-background)) |\n| `git` | Git 操作封装(status / diff / log / commit 等) |\n| `search_files` | 搜索文件内容 |\n| `search_sessions` | 跨历史会话语义搜索 |\n| `list_dir` | 列出目录内容 |\n| `run_skill` | 运行内置技能 |\n| `list_skills` | 列出可用技能 |\n| `run_code` | 编写并执行代码(Python/Node.js/Bash),超时 1-300s,输出截断 50KB |\n| `spawn_sub_agent` | 分派独立上下文的子代理(支持 `profile: explorer/executor/design`) |\n| `web_fetch` | 抓取 URL 内容(默认拒绝私网 / SSRF 防护) |\n| `web_search` | 联网搜索(7 个可插拔搜索源,`auto` 按密钥择优,详见[联网搜索](./web-search)) |\n| `notebook_edit` | 编辑 Jupyter `.ipynb` 单元格:按 `cell_id`(优先)或 `cell_index` 定位,`edit_mode` = replace(默认)/ insert / delete |\n| `todo_write` | 会话级待办清单(open-agents 对标) |\n| `ask_user_question` | 主动向用户提问并阻塞等待回答 |\n| `code_intelligence` | LSP 语义代码智能:定义 / 引用 / 诊断 / 重命名(详见 [code-intel](./cli-code-intel)) |\n| `slash_command` | 在会话内执行 REPL 斜杠命令 |\n| `notify` | 落一条通知意图到 `~/.chainlesschain/agent-schedule/`,由 [cc agenda](./cli-agenda) 消费触发 |\n| `schedule` | 落一条定时任务意图(同上,agenda 调度执行) |\n| `publish_artifact` | 把完工交付物发布进个人交付物库,只回传元数据(详见 [Artifacts](./cli-artifacts)) |\n| `browser_state` | 观察用户 Chrome 当前状态(CDP,需先 `cc browse chrome launch`):URL/标题/标签页 + 观察窗口 console/失败网络请求 + DOM 快照(40k cap)+ 可选截图;只读不操控,仅 loopback |\n\n### run_code 工具详情\n\n`run_code` 是 v0.40.3 新增的代码执行工具,让 AI 能主动编写脚本解决用户问题。\n\n**参数**:\n| 参数 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `language` | string | 是 | 编程语言:`python`、`node`、`bash` |\n| `code` | string | 是 | 要执行的代码 |\n| `timeout` | number | 否 | 超时时间(秒),默认 60,最大 300 |\n\n**执行流程**:\n\n1. 代码写入临时文件(`os.tmpdir()` + 时间戳)\n2. 根据语言选择解释器(`python3`/`python`、`node`、`bash`)\n3. 执行并捕获输出(最大 50KB)\n4. 返回 `{ success, output, language, duration }` 或错误信息\n5. 清理临时文件\n\n**语言选择建议**:\n\n- **Python**: 数据处理、数学计算、Web 抓取、文件批量操作\n- **Node.js**: JSON 处理、API 调用、npm 生态相关\n- **Bash**: 系统管理、文件操作、管道命令\n\n**使用示例**:\n\n```\n> 帮我统计当前目录下每种文件类型的数量\n\n🤖 好的,我来写个脚本统计:\n[run_code: node (156 chars)]\n .js: 42\n .json: 8\n .md: 15\n .test.js: 23\n共 88 个文件\n```\n\n### notebook_edit 工具详情\n\n`notebook_edit` 补齐了 cc 此前缺失的最后一个 Claude Code 代理工具(NotebookEdit),用于编辑 Jupyter `.ipynb` 笔记本的单元格。纯函数 `editNotebookCell()` 实现:解析 → 变更 → 以 nbformat 规范(1 空格缩进 + 末尾换行)重新序列化。\n\n**参数**:\n| 参数 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `notebook_path` | string | 是 | `.ipynb` 文件路径 |\n| `cell_id` | string | 否* | 目标单元格 id(**优先**) |\n| `cell_index` | number | 否* | 0 基单元格下标(无 `cell_id` 时用) |\n| `edit_mode` | string | 否 | `replace`(默认)/ `insert` / `delete` |\n| `new_source` | string | replace/insert 必填 | 新单元格源码 |\n\n> \\*`replace` / `delete` 须给 `cell_id` 或 `cell_index` 之一定位目标。\n\n**语义**:源码以行数组存储并保留末尾 `\\n`;`replace` 一个代码单元会**清空其 outputs 与 execution_count**;`insert` 会**生成新的 cell id**(插在目标之后,或无定位时插到顶部)。完善的错误路径:坏 JSON / 无 `cells[]` / 缺定位符 / 目标不存在 / 缺参数。\n\n## 沙箱网络隔离与出口过滤代理 (Phase 1)\n\n当 `run_shell` 在**沙箱模式**下执行(Docker 或 bubblewrap 后端)时,其网络出口由一套**策略判定 + 强制过滤**的两层机制守卫——把「agent 跑的 shell 命令能访问哪些域名」从提示词约束升级为**环境强制**。\n\n### 策略判定(`sandbox-network-policy.js`)\n\n纯共享逻辑、独立可测,决定一个目标是否放行:\n\n- **`extractHost`**: 从 URL / `host:port` / IPv6 提取主机,**归一化方括号**(修 `new URL()` 对 `[::1]` 漏括号的 SSRF 坑)。\n- **`matchesDomain`**: 支持 `*`(全放行公网)/ `*.example.com`(含 apex)/ 精确匹配,防后缀欺骗。\n- **`isPrivateHost`**: 识别 loopback / `10.` / `172.16-31` / `192.168` / `169.254` / `::1` / `fc00::` / `fe80::` / 云 metadata / IPv4-mapped。\n- **`evaluateNetworkAccess`**: 优先级 `deny > 具体 allow > 私网守卫 > \"*\" allow > 默认拒`。裸 `*` 放行公网但**不含**内网/metadata;显式白名单可覆盖私网守卫(供 dev)。\n\n### 出口过滤代理(`sandbox-egress-proxy.js`)\n\n把「判定」变「强制」:一个本地转发代理,对**每个** HTTP 请求 + HTTPS `CONNECT` 隧道调 `evaluateNetworkAccess`,拒绝的 host 直接 403 / 关隧道(**上游零接触**),私网/metadata 即使 `*` allow 也拦(SSRF 防护)。沙箱进程的 `HTTP(S)_PROXY` 指向它,其 `curl` / `npm` / `pip` / `git` / `wget` / `fetch` 出口即被过滤。\n\n- **Docker 后端**: 注入 `--add-host host.docker.internal:host-gateway` + `HTTP(S)_PROXY`。\n- **bubblewrap 后端**: `--share-net` + `HTTP(S)_PROXY` 指向 `127.0.0.1`。\n- **无网络**: 不启用网络时 Docker 加 `--network none`。\n- **fail-closed**: 配了域名限制但代理起不来 → **拒绝**无限制网络访问(绝不静默放行)。\n\n### 配置\n\n```jsonc\n{\n \"sandbox\": {\n \"network\": true, // 启用网络(false → --network none 完全断网)\n \"network.allowedDomains\": [\"registry.npmjs.org\", \"*.github.com\"],\n \"network.deniedDomains\": [\"evil.example.com\"],\n },\n}\n```\n\n配了 `allowedDomains`/`deniedDomains` 且 `network:true` 时,`run_shell` 自动起代理 → 跑命令 → 拆代理。\n\n> **诚实标注(跨平台但非内核级)**: 这是**代理层**控制,Windows 可验、免 root、跨平台。忽略 `HTTP(S)_PROXY` 的工具(或直接 syscall 建连的程序)仍需 OS 级 netns 隔离(Linux bwrap netns / macOS Seatbelt / Windows AppContainer)才能真正封死——那部分属平台相关的内核硬隔离。\n\n## Auto Pip-Install (自动安装 Python 包)\n\n> v0.40.3+ 新增\n\n当 `run_code` 工具执行 Python 代码遇到 `ModuleNotFoundError` 时,会自动尝试 `pip install` 安装缺失的包,然后重新执行脚本。\n\n### 工作流程\n\n1. 执行 Python 脚本 → 捕获 `ModuleNotFoundError: No module named 'pandas'`\n2. 从错误消息中提取包名(取顶层名称:`foo.bar` → `foo`)\n3. 通过 `isValidPackageName()` 验证包名安全性(拒绝 Shell 元字符)\n4. 执行 `python3 -m pip install pandas`(超时 120s)\n5. 重新执行脚本,返回结果\n\n### 返回示例\n\n```json\n{\n \"success\": true,\n \"output\": \"DataFrame shape: (100, 5)\\n...\",\n \"language\": \"python\",\n \"duration\": \"1523ms\",\n \"autoInstalled\": [\"pandas\"],\n \"scriptPath\": \".chainlesschain/agent-scripts/2026-03-15-10-30-45-python.py\"\n}\n```\n\n### 安全防护\n\n`isValidPackageName()` 使用正则 `/^[a-zA-Z0-9_][a-zA-Z0-9._-]*$/` 验证包名:\n\n- ✅ 合法:`pandas`, `scikit-learn`, `python_dotenv`, `Flask`\n- ❌ 拒绝:`foo; rm -rf /`, `$(whoami)`, `foo|bar`, 空字符串, 超过 100 字符\n\n## Script Persistence (脚本持久化)\n\n> v0.40.3+ 新增\n\n`run_code` 工具默认将脚本保存到项目目录下,方便后续查看和复用。\n\n### 持久化模式(默认)\n\n```\n.chainlesschain/agent-scripts/\n├── 2026-03-15-10-30-45-python.py\n├── 2026-03-15-10-31-02-node.js\n└── 2026-03-15-10-32-18-bash.sh\n```\n\n- 文件名格式:`YYYY-MM-DD-HH-MM-SS-{language}.{ext}`\n- 自动创建 `.chainlesschain/agent-scripts/` 目录\n- 返回结果中 `scriptPath` 字段指向保存路径\n\n### 临时模式(`persist: false`)\n\n- 脚本写入 `os.tmpdir()`\n- 执行完毕后自动清理(`finally` 块中 `unlinkSync`)\n- 返回结果中 `scriptPath` 为 `undefined`\n\n## Error Classification (错误分类)\n\n> v0.40.3+ 新增\n\n`run_code` 执行失败时,`classifyError()` 函数将错误分为 5 种类型,每种附带 `hint` 字段提供可操作的修复建议。\n\n| errorType | 匹配模式 | hint 示例 |\n| ------------------ | --------------------------------------- | ------------------------------------------------------------ |\n| `import_error` | `ModuleNotFoundError`, `ImportError` | `Missing Python module \"pandas\". Will attempt auto-install.` |\n| `syntax_error` | `SyntaxError`, `IndentationError` | `Syntax error on line 15. Check for typos.` |\n| `timeout` | `ETIMEDOUT`, `timed out`, exitCode=null | `Script timed out. Consider increasing timeout.` |\n| `permission_error` | `EACCES`, `Permission denied` | `Permission denied. Try a different directory.` |\n| `runtime_error` | 其他所有错误 | `Runtime error near line 42. Check traceback.` |\n\n### 返回示例(错误)\n\n```json\n{\n \"error\": \"ModuleNotFoundError: No module named 'pandas'\",\n \"stderr\": \"...\",\n \"exitCode\": 1,\n \"language\": \"python\",\n \"errorType\": \"import_error\",\n \"hint\": \"Missing Python module \\\"pandas\\\". Will attempt auto-install.\",\n \"scriptPath\": \".chainlesschain/agent-scripts/2026-03-15-10-30-45-python.py\"\n}\n```\n\n## Environment Detection (环境检测)\n\n> v0.40.3+ 新增\n\n`getEnvironmentInfo()` 在进程启动时检测运行环境信息,缓存后注入到 System Prompt 中,帮助 LLM 感知宿主环境。\n\n### 检测项\n\n| 字段 | 说明 | 示例 |\n| -------- | ------------- | ---------------------------- |\n| `os` | 操作系统 | `win32`, `linux`, `darwin` |\n| `arch` | 架构 | `x64`, `arm64` |\n| `python` | Python 解释器 | `python3 (3.11.0)` 或 `null` |\n| `pip` | pip 是否可用 | `true` / `false` |\n| `node` | Node.js 版本 | `v22.12.0` |\n| `git` | Git 是否可用 | `true` / `false` |\n\n### System Prompt 注入\n\n```\n## Environment\nOS: win32 (x64)\nPython: python3 (3.11.0) + pip\nNode.js: v22.12.0\nGit: available\n```\n\n### 架构图\n\n```\nagent-core.js (中心模块)\n │\n ├── AGENT_TOOLS (19 工具定义)\n ├── getBaseSystemPrompt() ← getEnvironmentInfo()\n ├── executeTool() → executeToolInner() → _executeRunCode()\n │ ├── classifyError()\n │ ├── isValidPackageName()\n │ └── getCachedPython()\n ├── chatWithTools() → 10 LLM providers\n ├── agentLoop() → 异步生成器 (slot-filling + tool loop)\n └── formatToolArgs()\n```\n\n## Context Engineering\n\n代理模式集成了轻量级 Context Engineering 适配器(`cli-context-engineering.js`),在每次 LLM 调用前自动构建优化的上下文消息。\n\n### 6 维上下文注入\n\n| 注入器 | 数据源 | 说明 |\n| ---------------------- | ----------------------------- | -------------------------------------------------- |\n| **Instinct** | `instinct-manager.js` | 从学习到的用户偏好生成提示(编码风格、工具偏好等) |\n| **Memory** | `hierarchical-memory.js` | 召回与当前查询相关的四层记忆(working→core) |\n| **BM25 Notes** | `bm25-search.js` + `notes` 表 | 搜索笔记库中的相关内容(topK: 3, threshold: 0.5) |\n| **Task Reminder** | 内置 | 重述当前任务目标和进度,保持 AI 聚焦 |\n| **Permanent Memory** | `permanent-memory.js` | 跨会话持久记忆 + Daily Notes + MEMORY.md |\n| **Compaction Summary** | 内置 | 被压缩消息的单行摘要,支持上下文恢复 |\n\n### 稳定前缀缓存 (Stable Prefix Cache)\n\nContext Engineering 按固定顺序排列注入内容(System Prompt → Instinct → Memory → Notes → Task → Permanent Memory),确保 LLM 的 KV-Cache 前缀尽量稳定。当上下文内容不变时,LLM 可复用已缓存的计算结果,显著降低推理延迟。\n\n### System Prompt 优化\n\n- 清理时间戳 → `[DATE]`、UUID → `[UUID]`、session ID → `[SESSION]`\n- 提升 KV-Cache 命中率,减少重复计算\n\n### 错误学习\n\n- 自动记录最近 10 条错误及解决方案\n- 注入到上下文中,避免 AI 重复犯错\n\n### 智能压缩 (`/compact`)\n\n基于重要性评分的对话压缩(替代简单的 \"保留最后 4 条\"):\n\n- 始终保留 System Prompt\n- 评分维度:时间近、含 tool_calls、关联任务目标\n- 默认保留得分最高的 6 对消息\n\n### 优雅降级\n\n- 无 DB 时自动跳过所有注入,使用静态 System Prompt\n- 每个注入步骤独立 try/catch,单个失败不影响其他\n\n## 斜杠命令\n\n> **`@` 路径补全(v0.162.38+)**:交互 REPL 中输入 `@` 后按 TAB 补全文件/目录路径(目录带尾斜杠可继续下钻,自动排除 `node_modules`/`.git`,大小写不敏感);IDE 桥接已连接时,编辑器打开的标签页混入候选并排最前(workspace 内转相对路径)。IDE 列表 5s TTL 后台刷新,补全本身同步零延迟。\n>\n> **输入排队(v0.162.40+)**:agent 回合进行中继续打字不再触发并发回合 —— 多余输入按 FIFO 排队(灰色 `queued (N)` 提示),当前回合结束后自动依次执行。\n>\n> **Esc 打断(v0.162.40+)**:回合进行中按 `Esc` 立即中止当前回合(黄色 \"turn interrupted\" 提示,非报错)——已产出的部分对话保留,排队中的输入照常继续执行。\n>\n> **会话回退 `/rewind`(v0.162.40+,steering 三件套收口)**:`/rewind` 列出历史用户轮次(新→旧编号 + 预览),`/rewind ` 把对话截回该轮之前并把原文预填到输入行(改完重发);空闲时**双击 Esc** 调出同一选择器。注意这只回退**对话**,文件状态回退用 `cc checkpoint restore`。\n>\n> **恢复摘要(v0.162.40+)**:`--resume` 重建对话后,REPL 自动打印一段**离线提取式**回顾(轮次统计 + 最后提问/回复预览,零 LLM 调用)—— 秒级回想\"刚才聊到哪了\"。\n>\n> **`!` bash 直通(v0.162.41+)**:`! ` 直接跑 shell(不经 LLM),输出回灌对话上下文(`/` 块),模型下一轮可见;Windows 走 `chcp 65001` 防乱码,120s 超时 / 30K 输出截断。\n>\n> **`#` 快捷记忆(v0.162.41+)**:`# <备注>` 一键追加到 git 根的 `cc.md`(`## Notes` 段、最新在上),本会话立即生效、之后的会话经[项目记忆](./cli-init#项目记忆)自动加载。\n>\n> **`/` 命令补全(v0.162.41+)**:行首输入 `/he` 补全 REPL 命令(命令 token 输入期间生效,空格后不打扰参数)。\n\n### 基础命令\n\n| 命令 | 说明 |\n| ---------------------- | ------------------------------------------------------------------------------------------------------- |\n| `/exit` | 退出代理 |\n| `/help` | 显示所有命令帮助 |\n| `/model ` | 切换/查看模型 |\n| `/provider

` | 切换/查看提供商,支持 8 个提供商 (ollama/anthropic/openai/deepseek/dashscope/mistral/gemini/volcengine) |\n| `/clear` | 清空对话历史 |\n| `/compact` | 智能压缩对话(基于重要性评分) |\n| `/rewind [n]` | 列出/回退到历史用户轮次(双击 Esc 同效;文件回退用 `cc checkpoint`) |\n| `! ` | bash 直通:直接跑 shell,输出进对话上下文 |\n| `# ` | 快捷记忆:追加到项目 `cc.md` 的 `## Notes` |\n| `/output-style [name]` | 列出 / 切换 [输出风格](./output-styles) 人格;`none` 清除 |\n\n### Context Engineering 命令\n\n| 命令 | 说明 |\n| ---------------------- | ---------------------------- |\n| `/task ` | 设置当前任务目标 |\n| `/task clear` | 清除任务 |\n| `/session` | 显示当前 Session ID 和消息数 |\n| `/session resume ` | 加载历史会话消息 |\n| `/reindex` | 重新索引笔记到 BM25 |\n| `/stats` | 显示 Context Engine 统计信息 |\n\n### Plan Mode 命令\n\n| 命令 | 说明 |\n| --------------- | ----------------------- |\n| `/plan` | 进入规划模式 |\n| `/plan show` | 查看当前计划 |\n| `/plan approve` | 批准计划并开始执行 |\n| `/plan reject` | 拒绝计划,重新规划 |\n| `/plan exit` | 退出规划模式 |\n| `/plan execute` | 按 DAG 依赖顺序执行计划 |\n| `/plan risk` | 显示风险评估报告 |\n\n### Cowork 命令\n\n| 命令 | 说明 |\n| -------------------------- | ------------------ |\n| `/cowork debate ` | 多视角代码评审 |\n| `/cowork compare ` | A/B 方案对比 |\n| `/cowork graph` | ASCII 代码知识图谱 |\n| `/cowork decision` | 决策追踪知识库 |\n\n### 自主模式命令\n\n| 命令 | 说明 |\n| -------------- | ------------------------- |\n| `/auto ` | 提交目标,AI 自主分解执行 |\n| `/auto status` | 查看当前自主目标进度 |\n| `/auto pause` | 暂停自主执行 |\n| `/auto resume` | 恢复自主执行 |\n| `/auto cancel` | 取消当前目标 |\n| `/auto list` | 列出所有目标(含历史) |\n\n## Plan Mode\n\n代理模式内置 Plan Mode(规划模式),让 AI 在执行复杂任务前先制定计划并获得用户审批。\n\n### 规划模式工作流\n\n1. 用户输入 `/plan` 或 AI 自动判断需要规划\n2. AI 进入只读模式(仅允许 `read_file`、`search_files`、`list_dir`)\n3. AI 生成结构化计划,包含步骤、依赖和风险评估\n4. 用户审批(`/plan approve`)后 AI 开始执行\n5. 用户可随时拒绝(`/plan reject`)要求重新规划\n\n### 使用示例\n\n```\n> /plan\n\n📋 已进入规划模式。请描述您的任务。\n\n> 重构认证模块,从 session 迁移到 JWT\n\n🤖 正在分析代码库...\n[只读: read_file auth/session.js]\n[只读: search_files \"authentication\"]\n\n📋 计划:\n1. 创建 JWT 工具模块\n2. 修改登录接口返回 JWT token\n3. 添加 JWT 验证中间件\n4. 更新受保护路径\n5. 移除 session 依赖\n6. 添加单元测试\n\n> /plan approve\n✅ 开始执行计划...\n```\n\n## 自主模式 (Autonomous Agent)\n\n代理模式内置自主模式,用户只需提交高层目标,AI 自动分解为子任务并循环执行,无需手动逐步指令。\n\n### ReAct 循环\n\n自主模式采用 ReAct(Reason → Act → Observe)循环:\n\n1. **Reason**: AI 分析当前目标和已完成的子任务,决定下一步行动\n2. **Act**: 调用工具(读写文件、执行命令、搜索等)执行子任务\n3. **Observe**: 观察工具输出,评估结果是否符合预期\n4. **重复**: 回到 Reason,直到目标完成或达到 Token 预算上限\n\n### 核心特性\n\n- **目标分解**: 自动将高层目标拆分为可执行的子任务序列\n- **自我纠错**: 工具调用失败时自动分析原因并尝试替代方案\n- **Token 预算**: 可配置的 Token 消耗上限,防止无限循环\n- **进度追踪**: `/auto status` 实时查看已完成/剩余子任务\n- **暂停/恢复**: `/auto pause` 和 `/auto resume` 控制执行节奏\n\n### 使用示例\n\n```\n> /auto 为项目添加完整的 CI/CD 配置(GitHub Actions + Docker)\n\n🤖 目标已接收,正在分解...\n 子任务 1: 分析项目结构和依赖\n 子任务 2: 创建 .github/workflows/ci.yml\n 子任务 3: 创建 Dockerfile\n 子任务 4: 创建 docker-compose.yml\n 子任务 5: 添加测试和构建步骤\n 子任务 6: 验证配置正确性\n\n[Reason] 开始分析项目结构...\n[Act] read_file: package.json\n[Observe] Node.js 项目,使用 Vitest 测试...\n[Reason] 需要创建 CI 工作流...\n[Act] write_file: .github/workflows/ci.yml\n...\n\n✅ 目标完成!已创建 4 个文件,所有配置已验证。\n\n> /auto status\n目标: 为项目添加完整的 CI/CD 配置\n状态: ✅ 已完成 (6/6 子任务)\nToken 消耗: 12,450 / 50,000\n```\n\n## SlotFiller 意图检测\n\n> v0.41.1 新增\n\nAgent 模式集成了 SlotFiller 参数槽填充引擎,在调用 LLM 之前自动检测用户消息中的已知意图,补全缺失的必需参数。\n\n### 支持的意图类型\n\n| 意图类型 | 触发关键词 | 必需参数 |\n| ------------- | --------------------------------- | -------------- |\n| `create_file` | create a file, new file, scaffold | fileType, path |\n| `deploy` | deploy, ship it, push to prod | platform |\n| `refactor` | refactor, restructure | target |\n| `test` | write tests, add test, unit test | target |\n| `analyze` | analyze, audit, review code | — |\n| `search` | search for, find all, grep | query |\n| `install` | install, add package | package |\n| `generate` | generate, create component | — |\n| `edit_file` | edit file, modify file | target |\n\n### 工作流程\n\n1. 用户输入消息(如 \"deploy this\")\n2. `CLISlotFiller.detectIntent()` 检测到 `deploy` 意图\n3. 检查必需参数 `platform` 缺失\n4. 通过 InteractionAdapter 向用户提问 \"Which platform?\"\n5. 用户回答 \"docker\"\n6. 补全结果注入到用户消息末尾:`[Context — user provided: platform: docker]`\n7. 带有完整上下文的消息发送给 LLM\n\n### 使用示例\n\n```\n> deploy this\n\n? Which platform do you want to deploy to?\n > docker\n > vercel\n > aws\n > heroku\n\n[SlotFiller] platform = \"docker\"\n\n🤖 好的,我来帮你部署到 Docker...\n[run_shell: docker build -t myapp .]\n```\n\n未识别的意图(如 \"help me understand this code\")不会触发 SlotFiller,直接传给 LLM 处理。\n\n## 使用示例\n\n### 基础使用\n\n```\n> 帮我重构 src/utils/helper.js,提取公共方法\n\n🤖 我来分析这个文件...\n[调用 read_file: src/utils/helper.js]\n[调用 edit_file: 提取公共方法到 src/utils/common.js]\n[调用 run_skill: code-review 检查重构质量]\n\n已完成重构,提取了 3 个公共方法到 common.js。\n```\n\n### 使用火山引擎(豆包)Agent 模式\n\n```bash\n# 设置环境变量\nexport VOLCENGINE_API_KEY=ark-xxxxxxxxxxxx\n\n# 启动 Agent(默认旗舰模型 doubao-seed-1-6-251015)\nchainlesschain agent --provider volcengine\n\n# 指定代码模型\nchainlesschain agent --provider volcengine --model doubao-seed-1-6-251015\n```\n\nAgent 模式下会自动根据任务类型智能选择最佳模型:\n\n```\nyou> 写一个 Python 快速排序函数\n[auto] 代码任务 → doubao-seed-1-6-251015\nai> def quicksort(arr): ...\n\nyou> 分析 TCP 三次握手的过程\n[auto] 复杂推理 → doubao-seed-1-6-251015\nai> TCP三次握手过程如下:...\n\nyou> 今天天气怎么样?\nai> (使用默认模型,快速响应)\n```\n\n### 使用 OpenAI / DeepSeek Agent 模式\n\n```bash\n# OpenAI\nexport OPENAI_API_KEY=sk-xxxxxxxxxxxx\nchainlesschain agent --provider openai --model gpt-4o\n\n# DeepSeek\nexport DEEPSEEK_API_KEY=sk-xxxxxxxxxxxx\nchainlesschain agent --provider deepseek --model deepseek-chat\n```\n\n### 通过中转站使用 Agent 模式(无需科学上网)\n\n```bash\n# 通过 API2D 中转站使用 GPT-4o\nchainlesschain agent \\\n --provider openai \\\n --base-url https://oa.api2d.net/v1 \\\n --api-key fk-xxxxxx \\\n --model gpt-4o\n\n# 通过 One-API 网关\nchainlesschain agent \\\n --provider openai \\\n --base-url http://localhost:3000/v1 \\\n --api-key sk-oneapi-xxxxxx \\\n --model gpt-4o\n\n# 火山引擎通过代理\nchainlesschain agent \\\n --provider volcengine \\\n --base-url https://your-proxy.com/volcengine/v3 \\\n --model doubao-seed-1-6-251015\n\n# 企业 Nginx 反向代理\nchainlesschain agent \\\n --provider openai \\\n --base-url https://ai-gateway.company.com/v1 \\\n --model gpt-4o\n```\n\n> **提示**: `--base-url` 覆盖提供商默认地址,但保留 OpenAI 兼容的 API 格式。所有支持 `/chat/completions` 接口的中转站和代理均可使用。\n\n### Context Engineering 使用\n\n```\n> /task 重构认证模块\n\n✓ Task set: 重构认证模块\n\n> 查看当前进度\n\n🤖 当前任务: 重构认证模块\n[Context Engine 自动注入任务提醒 + 相关记忆 + 用户偏好]\n...\n\n> /stats\n\nDB connected: true\nNotes indexed: 42\nError history: 0\nActive task: true\n```\n\n### 会话恢复\n\n```bash\n# 第一次会话\nchainlesschain agent\n> 帮我写一个 API 服务\n> /exit\n\n# 恢复上次会话\nchainlesschain agent --session session-17...\nResumed session session-17... (8 messages)\n> 继续上次的工作\n```\n\n## 关键文件\n\n- `packages/cli/src/commands/agent.js` — 命令入口(含 `--session` 选项)\n- `packages/cli/src/repl/agent-repl.js` — 代理 REPL(9 工具 + 146 技能 + Plan Mode + Context Engineering)\n- `packages/cli/src/lib/cli-context-engineering.js` — Context Engineering 适配器(6 维注入 + 智能压缩)\n- `packages/cli/src/lib/plan-mode.js` — Plan Mode 实现\n- `packages/cli/src/lib/instinct-manager.js` — Instinct 学习引擎\n- `packages/cli/src/lib/hierarchical-memory.js` — 四层记忆系统\n- `packages/cli/src/lib/bm25-search.js` — BM25 搜索引擎\n- `packages/cli/src/lib/session-manager.js` — 会话持久化\n- `packages/cli/src/lib/permanent-memory.js` — 跨会话持久记忆(Daily Notes + MEMORY.md + BM25 混合搜索)\n- `packages/cli/src/lib/autonomous-agent.js` — ReAct 自主任务循环\n- `packages/cli/src/lib/content-recommender.js` — TF-IDF 工具相似度 + 工具链推荐\n- `packages/cli/src/lib/hook-manager.js` — Hook 管道(PreToolUse/PostToolUse/ToolError)\n- `packages/cli/src/lib/slot-filler.js` — SlotFiller 参数槽填充(意图检测 + 交互式补全)\n- `packages/cli/src/lib/agent-core.js` — Agent 核心业务逻辑(agentLoop 异步生成器)\n- `packages/cli/src/runtime/bootstrap.js` — 7 阶段无头启动\n\n## 安全考虑\n\n- `run_shell` 工具可执行任意 Shell 命令,请在可信环境中使用\n- `run_code` 工具可执行 Python/Node.js/Bash 脚本,代码在临时文件中执行并自动清理\n- `write_file` / `edit_file` 可修改文件系统,建议在 Git 仓库中使用以便回滚\n- Plan Mode 默认只读,需用户审批后才执行写操作\n- API Key 仅存储在本地,不会通过工具调用泄露\n- Context Engineering 注入的记忆/笔记仅在本地处理,不泄露到外部\n\n## 配置参考\n\n### Agent REPL 选项\n\n```javascript\n// packages/cli/src/repl/agent-repl.js\n{\n provider: \"ollama\", // ollama | anthropic | openai | deepseek |\n // dashscope | mistral | gemini | volcengine\n model: \"qwen2.5:7b\", // 模型名称\n baseUrl: \"http://localhost:11434\", // API 基础 URL\n apiKey: null, // API 密钥(云端提供商必填)\n sessionId: null, // 恢复指定会话 ID\n noRecallMemory: false, // 禁用启动时记忆召回\n recallLimit: 5, // 启动时注入的记忆条数上限\n noStream: false, // 禁用流式输出渲染\n noParkOnExit: false, // 退出时关闭而非 park 会话\n agentId: null, // 限定记忆召回范围的 agent ID\n}\n```\n\n### Context Engineering 选项\n\n```javascript\n// packages/cli/src/lib/cli-context-engineering.js\n{\n enableInstinct: true, // 注入用户偏好(Instinct)\n enableMemory: true, // 注入层次化记忆(top-5 相关记忆)\n enableBm25Notes: true, // 注入 BM25 笔记搜索结果\n bm25TopK: 3, // BM25 返回结果数\n bm25Threshold: 0.5, // BM25 相关性阈值\n enableTaskReminder: true, // 注入任务目标提醒\n enablePermanentMemory: true, // 注入跨会话持久记忆\n enableCompactionSummary: true, // 注入压缩摘要\n compactKeepCount: 6, // 智能压缩保留的消息对数\n}\n```\n\n### Hook 管道选项\n\n```javascript\n// packages/cli/src/lib/hook-manager.js\n// 通过 chainlesschain hook add 注册\n{\n event: \"PreToolUse\", // PreToolUse | PostToolUse | ToolError |\n // SessionStart | UserPromptSubmit | SessionEnd\n command: \"node my-hook.js\", // 钩子命令\n priority: 0, // 执行优先级(数值越小越先执行)\n timeout: 5000, // 超时时间(ms)\n}\n```\n\n### Plan Mode 选项\n\n```javascript\n// packages/cli/src/lib/plan-mode.js\n{\n autoEnter: false, // 是否在复杂任务时自动进入规划模式\n readOnlyTools: [ // 规划阶段允许使用的只读工具\n \"read_file\", \"search_files\", \"list_dir\"\n ],\n requireApproval: true, // 执行前必须用户审批\n enableDagExecution: true, // 按 DAG 依赖拓扑顺序执行步骤\n enableRiskAssessment: true, // 执行前显示风险评估报告\n}\n```\n\n### 自主模式选项\n\n```javascript\n// packages/cli/src/lib/autonomous-agent.js\n{\n tokenBudget: 50000, // 单目标最大 token 消耗\n maxSubtasks: 20, // 子任务数量上限\n autoRetry: true, // 工具失败时自动重试\n retryLimit: 3, // 单工具最大重试次数\n}\n```\n\n## 性能指标\n\n### 启动与初始化\n\n| 指标 | 数值 | 说明 |\n| -------------------------------- | ------ | -------------------------------------- |\n| 7 阶段无头启动(`bootstrap.js`) | ~200ms | DB 初始化 + 配置加载 + 技能注册 |\n| Context Engineering 首次初始化 | ~50ms | BM25 索引加载 + 记忆模型预热 |\n| 环境检测(`getEnvironmentInfo`) | ~30ms | 检测 Python/Node/Git,结果缓存 |\n| 138 个技能元数据加载(stub) | ~15ms | 仅加载 YAML frontmatter,body 按需读取 |\n\n### 工具执行性能\n\n| 工具 | 典型延迟 | 备注 |\n| ----------------------------- | ---------- | ----------------------- |\n| `read_file` | < 5ms | 本地文件读取 |\n| `write_file` / `edit_file` | < 10ms | 含 fsync |\n| `edit_file_hashed` | < 5ms | 哈希锚定,无需全文扫描 |\n| `search_files` | 10–200ms | 取决于代码库大小 |\n| `run_shell` | 视命令而定 | 超时 60s,输出截断 30KB |\n| `run_code` (Python/Node) | 500ms–5s | 含解释器启动开销 |\n| `run_code` + auto pip-install | +5–15s | 含 `pip install` 耗时 |\n| `web_fetch` | 500ms–3s | 含 DNS + TCP + 页面抓取 |\n| `spawn_sub_agent` | 200ms+ | 独立上下文初始化 |\n\n### Context Engineering 注入开销\n\n| 注入器 | 典型延迟 | 说明 |\n| -------------------- | ----------- | ---------------------------- |\n| Instinct | < 2ms | 内存读取,已缓存 |\n| Memory(层次化记忆) | 5–20ms | BM25 + 向量相关性搜索 |\n| BM25 Notes | 10–30ms | topK=3,threshold=0.5 |\n| Task Reminder | < 1ms | 内存字段拼接 |\n| Permanent Memory | 5–15ms | MEMORY.md + Daily Notes 读取 |\n| Compaction Summary | < 1ms | 内存字段拼接 |\n| **6 维合计** | **20–70ms** | KV-Cache 稳定前缀可复用 |\n\n### 智能压缩效果\n\n| 指标 | 数值 |\n| -------------------- | ----------- |\n| 压缩率 | 0.76–0.93 |\n| 节省 token | 7–24% |\n| 压缩触发阈值 | 消息数 ≥ 12 |\n| 保留消息对数(默认) | 6 对 |\n\n## 测试覆盖率\n\n### 单元测试\n\n```\n✅ agent-core.test.js - 66 测试 (agentLoop / executeTool / chatWithTools)\n✅ hashline.test.js - 29 测试 (hashLine / annotateLines / findByHash / replaceByHash)\n✅ agent-core-edit-hashed.test.js - 12 测试 (edit_file_hashed handler)\n✅ slot-filler.test.js - 18 测试 (intent detection / slot filling / interaction)\n✅ plan-mode.test.js - 22 测试 (DAG 执行 / 风险评估 / 审批流)\n✅ autonomous-agent.test.js - 20 测试 (ReAct 循环 / 目标分解 / 自动重试)\n✅ context-engineering.test.js - 24 测试 (6 维注入 / 智能压缩 / 优雅降级)\n✅ session-hooks.test.js - 15 测试 (SessionStart / UserPromptSubmit / SessionEnd)\n✅ skill-mcp.test.js - 26 测试 (SKILL.md inline MCP / mount / unmount)\n```\n\n### 集成测试\n\n```\n✅ agent-repl-integration.test.js - 18 测试 (完整 REPL 生命周期 + 会话恢复)\n✅ run-code-integration.test.js - 16 测试 (Python / Node / Bash 执行 + auto pip-install)\n✅ context-engineering-e2e.test.js - 12 测试 (6 维注入 × 有 DB / 无 DB 场景)\n```\n\n### 验证结果(v5.0.2.10)\n\n| 验证项 | 结果 |\n| ----------------------- | ---------- |\n| `agent-core` 定向单测 | ✅ 66/66 |\n| `hashline` 单测 | ✅ 29/29 |\n| `edit_file_hashed` 单测 | ✅ 12/12 |\n| `slot-filler` 单测 | ✅ 18/18 |\n| `plan-mode` 单测 | ✅ 22/22 |\n| `session-hooks` 单测 | ✅ 15/15 |\n| `skill-mcp` 单测 | ✅ 26/26 |\n| Agent REPL 集成 | ✅ 18/18 |\n| `run-code` 集成 | ✅ 16/16 |\n| CLI 本轮定向合计 | ✅ 222/222 |\n\n## 故障排查\n\n| 问题 | 解决方案 |\n| -------------------- | ----------------------------------------------------- |\n| 工具调用失败 | 检查当前目录的文件权限 |\n| Plan Mode 不生效 | 输入 `/plan` 手动进入规划模式 |\n| 代理响应过慢 | 切换到更快的模型(如 `qwen2.5:7b`)或使用云端 API |\n| `run_shell` 权限不足 | 检查当前用户的 Shell 执行权限 |\n| Context 注入无效果 | 检查 DB 是否初始化(`chainlesschain db init`) |\n| `--session` 恢复失败 | 确认 Session ID 存在(`chainlesschain session list`) |\n| 记忆/笔记未注入 | 使用 `/stats` 检查 DB 连接和索引状态 |\n| `/auto` 无响应 | 检查 LLM 连接和 Token 预算 |\n| Hook 执行失败 | 使用 `chainlesschain hook stats` 检查钩子状态 |\n\n## 相关文档\n\n- [LLM 中转站与自定义接入](./cli-llm-proxy) — 中转站、代理、自建网关完整指南\n- [技能系统](./cli-skill) — 146 个内置技能\n- [Plan Mode](./plan-mode) — 桌面端 Plan Mode 详情\n- [AI 对话](./cli-chat) — 基础对话功能\n- [会话管理](./cli-session) — 代理会话持久化\n- [层次化记忆](./cli-hmemory) — 四层记忆系统\n- [Instinct 学习](./cli-instinct) — 用户偏好学习\n- [IDE 桥接](./ide-bridge) — 自动连接编辑器的 MCP server(`--ide` / `mcp__ide__*`)\n- [PDH Bridge 个人数据 IDE 桥接](./pdh-bridge) — 自动连接安卓 App 的设备采集(`--pdh` / `mcp__pdh__*`)\n"}}, "packages/cli/__tests__/integration/proxy-base-url-override.test.js": {"e8e7ba274b487ed491c04ec3359841a0e545debb": {"t": 1785809876.945656, "n": 19357, "h": "3ca1bab37b5dbf79", "c": "/**\n * Integration tests: Proxy / relay site (中转站) base-url override\n *\n * Verifies that --base-url correctly overrides provider default URLs,\n * allowing access through proxy/relay sites like API2D, OpenAI-SB, One-API, etc.\n */\n\nimport { describe, it, expect, vi, afterEach, beforeEach } from \"vitest\";\nimport {\n BUILT_IN_PROVIDERS,\n LLMProviderRegistry,\n} from \"../../src/lib/llm-providers.js\";\nimport { MockDatabase } from \"../helpers/mock-db.js\";\n\ndescribe(\"Proxy / Base-URL Override (中转站)\", () => {\n let capturedUrl;\n let capturedHeaders;\n let capturedBody;\n\n beforeEach(() => {\n capturedUrl = null;\n capturedHeaders = null;\n capturedBody = null;\n\n vi.stubGlobal(\n \"fetch\",\n vi.fn().mockImplementation((url, options) => {\n capturedUrl = url;\n capturedHeaders = options?.headers || {};\n capturedBody = options?.body ? JSON.parse(options.body) : null;\n return Promise.resolve({\n ok: true,\n json: () =>\n Promise.resolve({\n choices: [\n {\n message: {\n role: \"assistant\",\n content: \"proxy response\",\n },\n },\n ],\n }),\n body: {\n getReader: () => ({\n read: vi\n .fn()\n .mockResolvedValueOnce({\n done: false,\n value: new TextEncoder().encode(\n 'data: {\"choices\":[{\"delta\":{\"content\":\"proxy\"}}]}\\n\\ndata: [DONE]\\n',\n ),\n })\n .mockResolvedValueOnce({ done: true }),\n }),\n },\n });\n }),\n );\n });\n\n afterEach(() => {\n vi.unstubAllGlobals();\n vi.restoreAllMocks();\n });\n\n // ─── ask.js queryLLM-like flow with base-url override ──────\n\n describe(\"queryLLM with proxy base-url (ask command flow)\", () => {\n /**\n * Simulates the queryLLM flow from ask.js\n * Tests that --base-url overrides provider.baseUrl\n */\n async function simulateQueryLLM(question, options = {}) {\n const provider = options.provider || \"ollama\";\n const model = options.model || \"gpt-4o\";\n const baseUrl = options.baseUrl || \"http://localhost:11434\";\n\n if (provider === \"ollama\") {\n await fetch(`${baseUrl}/api/generate`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model, prompt: question, stream: false }),\n });\n } else {\n const providerDef = BUILT_IN_PROVIDERS[provider];\n const apiKey =\n options.apiKey ||\n (providerDef?.apiKeyEnv ? process.env[providerDef.apiKeyEnv] : null);\n const apiBase = options.baseUrl || providerDef?.baseUrl;\n await fetch(`${apiBase}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n model: model || providerDef?.models[0],\n messages: [{ role: \"user\", content: question }],\n }),\n });\n }\n }\n\n it(\"should use proxy URL when --base-url is provided for openai\", async () => {\n await simulateQueryLLM(\"hello\", {\n provider: \"openai\",\n baseUrl: \"https://oa.api2d.net/v1\",\n apiKey: \"fk-proxy-key\",\n model: \"gpt-4o\",\n });\n\n expect(capturedUrl).toBe(\"https://oa.api2d.net/v1/chat/completions\");\n expect(capturedHeaders.Authorization).toBe(\"Bearer fk-proxy-key\");\n expect(capturedBody.model).toBe(\"gpt-4o\");\n });\n\n it(\"should use proxy URL for volcengine with custom relay site\", async () => {\n await simulateQueryLLM(\"你好\", {\n provider: \"volcengine\",\n baseUrl: \"https://my-proxy.com/volcengine/v3\",\n apiKey: \"ark-proxy-key\",\n model: \"doubao-seed-1-6-251015\",\n });\n\n expect(capturedUrl).toBe(\n \"https://my-proxy.com/volcengine/v3/chat/completions\",\n );\n expect(capturedHeaders.Authorization).toBe(\"Bearer ark-proxy-key\");\n expect(capturedBody.model).toBe(\"doubao-seed-1-6-251015\");\n });\n\n it(\"should use proxy URL for deepseek via OpenAI-SB relay\", async () => {\n await simulateQueryLLM(\"test\", {\n provider: \"deepseek\",\n baseUrl: \"https://api.openai-sb.com/v1\",\n apiKey: \"sb-xxxxxx\",\n model: \"deepseek-chat\",\n });\n\n expect(capturedUrl).toBe(\"https://api.openai-sb.com/v1/chat/completions\");\n expect(capturedHeaders.Authorization).toBe(\"Bearer sb-xxxxxx\");\n });\n\n it(\"should fall back to provider default URL when no base-url is given\", async () => {\n process.env.OPENAI_API_KEY = \"test-key\";\n try {\n await simulateQueryLLM(\"test\", {\n provider: \"openai\",\n model: \"gpt-4o-mini\",\n });\n\n expect(capturedUrl).toBe(\"https://api.openai.com/v1/chat/completions\");\n } finally {\n delete process.env.OPENAI_API_KEY;\n }\n });\n\n it(\"should use proxy URL for One-API gateway\", async () => {\n await simulateQueryLLM(\"test\", {\n provider: \"openai\",\n baseUrl: \"http://localhost:3000/v1\",\n apiKey: \"sk-oneapi-xxxxxx\",\n model: \"gpt-4o\",\n });\n\n expect(capturedUrl).toBe(\"http://localhost:3000/v1/chat/completions\");\n expect(capturedHeaders.Authorization).toBe(\"Bearer sk-oneapi-xxxxxx\");\n });\n\n it(\"should use proxy URL for enterprise Nginx reverse proxy\", async () => {\n await simulateQueryLLM(\"test\", {\n provider: \"openai\",\n baseUrl: \"https://ai-gateway.company.com/v1\",\n apiKey: \"sk-enterprise-key\",\n model: \"gpt-4o\",\n });\n\n expect(capturedUrl).toBe(\n \"https://ai-gateway.company.com/v1/chat/completions\",\n );\n });\n\n it(\"should use Claude model through proxy with openai provider\", async () => {\n await simulateQueryLLM(\"test\", {\n provider: \"openai\",\n baseUrl: \"https://claude-proxy.com/v1\",\n apiKey: \"proxy-key\",\n model: \"claude-sonnet-4-20250514\",\n });\n\n expect(capturedUrl).toBe(\"https://claude-proxy.com/v1/chat/completions\");\n expect(capturedBody.model).toBe(\"claude-sonnet-4-20250514\");\n });\n });\n\n // ─── chat-repl flow with base-url override ─────────────────\n\n describe(\"chat-repl with proxy base-url\", () => {\n /**\n * Simulates the chat-repl URL resolution logic\n */\n function resolveChatUrl(provider, baseUrl) {\n const providerDef = BUILT_IN_PROVIDERS[provider];\n return baseUrl !== \"http://localhost:11434\"\n ? baseUrl\n : providerDef?.baseUrl || \"https://api.openai.com/v1\";\n }\n\n function resolveApiKey(provider, apiKey) {\n if (apiKey) return apiKey;\n const providerDef = BUILT_IN_PROVIDERS[provider];\n return providerDef?.apiKeyEnv ? process.env[providerDef.apiKeyEnv] : null;\n }\n\n it(\"should resolve proxy URL correctly for openai via relay site\", () => {\n const url = resolveChatUrl(\"openai\", \"https://oa.api2d.net/v1\");\n expect(url).toBe(\"https://oa.api2d.net/v1\");\n });\n\n it(\"should resolve proxy URL for volcengine via relay site\", () => {\n const url = resolveChatUrl(\n \"volcengine\",\n \"https://my-proxy.com/volcengine/v3\",\n );\n expect(url).toBe(\"https://my-proxy.com/volcengine/v3\");\n });\n\n it(\"should fall back to volcengine default URL when no proxy is set\", () => {\n const url = resolveChatUrl(\"volcengine\", \"http://localhost:11434\");\n expect(url).toBe(\"https://ark.cn-beijing.volces.com/api/v3\");\n });\n\n it(\"should resolve apiKey from --api-key over env var\", () => {\n process.env.VOLCENGINE_API_KEY = \"env-key\";\n try {\n const key = resolveApiKey(\"volcengine\", \"explicit-key\");\n expect(key).toBe(\"explicit-key\");\n } finally {\n delete process.env.VOLCENGINE_API_KEY;\n }\n });\n\n it(\"should resolve apiKey from env var when --api-key is null\", () => {\n process.env.VOLCENGINE_API_KEY = \"env-key\";\n try {\n const key = resolveApiKey(\"volcengine\", null);\n expect(key).toBe(\"env-key\");\n } finally {\n delete process.env.VOLCENGINE_API_KEY;\n }\n });\n\n it(\"should NOT use OPENAI_API_KEY for volcengine when apiKey is null\", () => {\n process.env.OPENAI_API_KEY = \"openai-key\";\n try {\n const key = resolveApiKey(\"volcengine\", null);\n // Should NOT return the openai key — this was the bug we fixed\n expect(key).not.toBe(\"openai-key\");\n expect(key).toBeFalsy();\n } finally {\n delete process.env.OPENAI_API_KEY;\n }\n });\n });\n\n // ─── agent-repl flow with base-url override ────────────────\n\n describe(\"agent-repl chatWithTools proxy URL resolution\", () => {\n /**\n * Simulates the agent-repl chatWithTools URL resolution\n */\n function resolveAgentUrl(provider, baseUrl) {\n const providerUrls = {\n openai: \"https://api.openai.com/v1\",\n deepseek: \"https://api.deepseek.com/v1\",\n dashscope: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n mistral: \"https://api.mistral.ai/v1\",\n gemini: \"https://generativelanguage.googleapis.com/v1beta/openai\",\n volcengine: \"https://ark.cn-beijing.volces.com/api/v3\",\n kimi: \"https://api.moonshot.cn/v1\",\n minimax: \"https://api.minimax.chat/v1\",\n };\n\n return baseUrl && baseUrl !== \"http://localhost:11434\"\n ? baseUrl\n : providerUrls[provider];\n }\n\n function resolveAgentApiKey(provider, apiKey) {\n if (apiKey) return apiKey;\n const envKeys = {\n openai: \"OPENAI_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n dashscope: \"DASHSCOPE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n gemini: \"GEMINI_API_KEY\",\n volcengine: \"VOLCENGINE_API_KEY\",\n kimi: \"MOONSHOT_API_KEY\",\n minimax: \"MINIMAX_API_KEY\",\n };\n const envKey = envKeys[provider] || \"OPENAI_API_KEY\";\n return process.env[envKey] || null;\n }\n\n it(\"should use proxy URL for all OpenAI-compatible providers\", () => {\n const providers = [\n \"openai\",\n \"deepseek\",\n \"dashscope\",\n \"mistral\",\n \"volcengine\",\n \"kimi\",\n \"minimax\",\n ];\n const proxyUrl = \"https://my-one-api-gateway.com/v1\";\n\n for (const p of providers) {\n const url = resolveAgentUrl(p, proxyUrl);\n expect(url).toBe(proxyUrl);\n }\n });\n\n it(\"should fall back to volcengine default when baseUrl is ollama default\", () => {\n const url = resolveAgentUrl(\"volcengine\", \"http://localhost:11434\");\n expect(url).toBe(\"https://ark.cn-beijing.volces.com/api/v3\");\n });\n\n it(\"should fall back to volcengine default when baseUrl is null\", () => {\n const url = resolveAgentUrl(\"volcengine\", null);\n expect(url).toBe(\"https://ark.cn-beijing.volces.com/api/v3\");\n });\n\n it(\"should resolve volcengine API key from env\", () => {\n process.env.VOLCENGINE_API_KEY = \"ark-test\";\n try {\n const key = resolveAgentApiKey(\"volcengine\", null);\n expect(key).toBe(\"ark-test\");\n } finally {\n delete process.env.VOLCENGINE_API_KEY;\n }\n });\n\n it(\"should prefer explicit apiKey over env var in agent mode\", () => {\n process.env.VOLCENGINE_API_KEY = \"env-key\";\n try {\n const key = resolveAgentApiKey(\"volcengine\", \"explicit-key\");\n expect(key).toBe(\"explicit-key\");\n } finally {\n delete process.env.VOLCENGINE_API_KEY;\n }\n });\n\n it(\"should build correct proxy request to relay site\", async () => {\n const proxyUrl = \"https://oa.api2d.net/v1\";\n const proxyKey = \"fk-relay-key\";\n const model = \"gpt-4o\";\n\n await fetch(`${proxyUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${proxyKey}`,\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"user\", content: \"hello\" }],\n tools: [{ type: \"function\", function: { name: \"read_file\" } }],\n }),\n });\n\n expect(capturedUrl).toBe(\"https://oa.api2d.net/v1/chat/completions\");\n expect(capturedHeaders.Authorization).toBe(\"Bearer fk-relay-key\");\n expect(capturedBody.tools).toBeDefined();\n expect(capturedBody.tools[0].function.name).toBe(\"read_file\");\n });\n });\n\n // ─── Custom provider with proxy URL ─────────────────────────\n\n describe(\"Custom provider registered with proxy URL\", () => {\n it(\"should register and test a custom provider with proxy base URL\", async () => {\n process.env.MY_PROXY_KEY = \"proxy-test-key\";\n try {\n const db = new MockDatabase();\n const registry = new LLMProviderRegistry(db);\n\n registry.addProvider(\"my-proxy\", {\n displayName: \"My OpenAI Proxy\",\n baseUrl: \"https://my-proxy.com/v1\",\n apiKeyEnv: \"MY_PROXY_KEY\",\n models: [\"gpt-4o\", \"claude-sonnet-4-20250514\"],\n });\n\n const provider = registry.get(\"my-proxy\");\n expect(provider).toBeTruthy();\n expect(provider.baseUrl).toBe(\"https://my-proxy.com/v1\");\n\n const result = await registry.testProvider(\"my-proxy\");\n expect(result.ok).toBe(true);\n expect(capturedUrl).toContain(\"my-proxy.com/v1\");\n } finally {\n delete process.env.MY_PROXY_KEY;\n }\n });\n\n it(\"should use proxy key from env for custom provider\", async () => {\n process.env.RELAY_KEY = \"relay-api-key\";\n try {\n const db = new MockDatabase();\n const registry = new LLMProviderRegistry(db);\n\n registry.addProvider(\"relay-site\", {\n displayName: \"Relay Site\",\n baseUrl: \"https://oa.api2d.net/v1\",\n apiKeyEnv: \"RELAY_KEY\",\n models: [\"gpt-4o\"],\n });\n\n const result = await registry.testProvider(\"relay-site\");\n expect(result.ok).toBe(true);\n expect(capturedHeaders.Authorization).toBe(\"Bearer relay-api-key\");\n } finally {\n delete process.env.RELAY_KEY;\n }\n });\n });\n\n // ─── Kimi and MiniMax proxy support ────────────────────────\n\n describe(\"Kimi and MiniMax proxy base-url override\", () => {\n it(\"should use proxy URL for kimi via relay site\", async () => {\n await fetch(\"https://my-proxy.com/moonshot/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer proxy-kimi-key\",\n },\n body: JSON.stringify({\n model: \"moonshot-v1-auto\",\n messages: [{ role: \"user\", content: \"hello\" }],\n }),\n });\n\n expect(capturedUrl).toBe(\n \"https://my-proxy.com/moonshot/v1/chat/completions\",\n );\n expect(capturedHeaders.Authorization).toBe(\"Bearer proxy-kimi-key\");\n expect(capturedBody.model).toBe(\"moonshot-v1-auto\");\n });\n\n it(\"should use proxy URL for minimax via relay site\", async () => {\n await fetch(\"https://my-proxy.com/minimax/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer proxy-minimax-key\",\n },\n body: JSON.stringify({\n model: \"MiniMax-Text-01\",\n messages: [{ role: \"user\", content: \"hello\" }],\n }),\n });\n\n expect(capturedUrl).toBe(\n \"https://my-proxy.com/minimax/v1/chat/completions\",\n );\n expect(capturedBody.model).toBe(\"MiniMax-Text-01\");\n });\n\n it(\"should fall back to kimi default URL when no proxy\", () => {\n const kimi = BUILT_IN_PROVIDERS.kimi;\n expect(kimi.baseUrl).toBe(\"https://api.moonshot.cn/v1\");\n });\n\n it(\"should fall back to minimax default URL when no proxy\", () => {\n const minimax = BUILT_IN_PROVIDERS.minimax;\n expect(minimax.baseUrl).toBe(\"https://api.minimax.chat/v1\");\n });\n });\n\n // ─── Anthropic and Gemini proxy (中转站) ──────────────────\n\n describe(\"Anthropic and Gemini proxy (中转站) support\", () => {\n it(\"should route anthropic-proxy through relay site with OpenAI format\", async () => {\n const proxyUrl = \"https://claude-relay.example.com/v1\";\n await fetch(`${proxyUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer relay-claude-key\",\n },\n body: JSON.stringify({\n model: \"claude-sonnet-4-6\",\n messages: [{ role: \"user\", content: \"test\" }],\n }),\n });\n\n expect(capturedUrl).toBe(\n \"https://claude-relay.example.com/v1/chat/completions\",\n );\n expect(capturedHeaders.Authorization).toBe(\"Bearer relay-claude-key\");\n expect(capturedBody.model).toBe(\"claude-sonnet-4-6\");\n });\n\n it(\"should route gemini-proxy through relay site with OpenAI format\", async () => {\n const proxyUrl = \"https://gemini-relay.example.com/v1\";\n await fetch(`${proxyUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer relay-gemini-key\",\n },\n body: JSON.stringify({\n model: \"gemini-2.0-flash\",\n messages: [{ role: \"user\", content: \"test\" }],\n }),\n });\n\n expect(capturedUrl).toBe(\n \"https://gemini-relay.example.com/v1/chat/completions\",\n );\n expect(capturedBody.model).toBe(\"gemini-2.0-flash\");\n });\n\n it(\"anthropic-proxy and gemini-proxy should use user-provided base URL\", () => {\n // These providers don't have a built-in entry — they're setup-wizard-only\n // The proxy URL is fully provided by the user during setup\n const { LLM_PROVIDERS } = require(\"../../src/constants.js\");\n expect(LLM_PROVIDERS[\"anthropic-proxy\"].isProxy).toBe(true);\n expect(LLM_PROVIDERS[\"anthropic-proxy\"].defaultBaseUrl).toBe(\"\");\n expect(LLM_PROVIDERS[\"gemini-proxy\"].isProxy).toBe(true);\n expect(LLM_PROVIDERS[\"gemini-proxy\"].defaultBaseUrl).toBe(\"\");\n });\n });\n\n // ─── Relay site URL format validation ──────────────────────\n\n describe(\"Relay site URL format validation\", () => {\n const relaySites = [\n { name: \"API2D\", url: \"https://oa.api2d.net/v1\" },\n { name: \"OpenAI-SB\", url: \"https://api.openai-sb.com/v1\" },\n { name: \"CloseAI\", url: \"https://api.closeai-proxy.xyz/v1\" },\n { name: \"AiHubMix\", url: \"https://aihubmix.com/v1\" },\n { name: \"One-API\", url: \"http://localhost:3000/v1\" },\n { name: \"Self-hosted Nginx\", url: \"https://ai-gateway.company.com/v1\" },\n ];\n\n for (const site of relaySites) {\n it(`${site.name} URL should form valid chat/completions endpoint`, () => {\n const endpoint = `${site.url}/chat/completions`;\n expect(endpoint).toMatch(/\\/v1\\/chat\\/completions$/);\n });\n }\n\n it(\"should correctly append /chat/completions to various URL formats\", () => {\n const urls = [\n \"https://proxy.com/v1\",\n \"https://proxy.com/v1/\",\n \"http://localhost:3000/v1\",\n ];\n\n for (const url of urls) {\n const base = url.replace(/\\/$/, \"\");\n expect(`${base}/chat/completions`).toMatch(/\\/chat\\/completions$/);\n }\n });\n });\n});\n"}, "4db48500206ff6a789c53c8197933113b06a6097": {"t": 1785809910.639951, "n": 19357, "h": "3ca1bab37b5dbf79", "c": "/**\n * Integration tests: Proxy / relay site (中转站) base-url override\n *\n * Verifies that --base-url correctly overrides provider default URLs,\n * allowing access through proxy/relay sites like API2D, OpenAI-SB, One-API, etc.\n */\n\nimport { describe, it, expect, vi, afterEach, beforeEach } from \"vitest\";\nimport {\n BUILT_IN_PROVIDERS,\n LLMProviderRegistry,\n} from \"../../src/lib/llm-providers.js\";\nimport { MockDatabase } from \"../helpers/mock-db.js\";\n\ndescribe(\"Proxy / Base-URL Override (中转站)\", () => {\n let capturedUrl;\n let capturedHeaders;\n let capturedBody;\n\n beforeEach(() => {\n capturedUrl = null;\n capturedHeaders = null;\n capturedBody = null;\n\n vi.stubGlobal(\n \"fetch\",\n vi.fn().mockImplementation((url, options) => {\n capturedUrl = url;\n capturedHeaders = options?.headers || {};\n capturedBody = options?.body ? JSON.parse(options.body) : null;\n return Promise.resolve({\n ok: true,\n json: () =>\n Promise.resolve({\n choices: [\n {\n message: {\n role: \"assistant\",\n content: \"proxy response\",\n },\n },\n ],\n }),\n body: {\n getReader: () => ({\n read: vi\n .fn()\n .mockResolvedValueOnce({\n done: false,\n value: new TextEncoder().encode(\n 'data: {\"choices\":[{\"delta\":{\"content\":\"proxy\"}}]}\\n\\ndata: [DONE]\\n',\n ),\n })\n .mockResolvedValueOnce({ done: true }),\n }),\n },\n });\n }),\n );\n });\n\n afterEach(() => {\n vi.unstubAllGlobals();\n vi.restoreAllMocks();\n });\n\n // ─── ask.js queryLLM-like flow with base-url override ──────\n\n describe(\"queryLLM with proxy base-url (ask command flow)\", () => {\n /**\n * Simulates the queryLLM flow from ask.js\n * Tests that --base-url overrides provider.baseUrl\n */\n async function simulateQueryLLM(question, options = {}) {\n const provider = options.provider || \"ollama\";\n const model = options.model || \"gpt-4o\";\n const baseUrl = options.baseUrl || \"http://localhost:11434\";\n\n if (provider === \"ollama\") {\n await fetch(`${baseUrl}/api/generate`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model, prompt: question, stream: false }),\n });\n } else {\n const providerDef = BUILT_IN_PROVIDERS[provider];\n const apiKey =\n options.apiKey ||\n (providerDef?.apiKeyEnv ? process.env[providerDef.apiKeyEnv] : null);\n const apiBase = options.baseUrl || providerDef?.baseUrl;\n await fetch(`${apiBase}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n model: model || providerDef?.models[0],\n messages: [{ role: \"user\", content: question }],\n }),\n });\n }\n }\n\n it(\"should use proxy URL when --base-url is provided for openai\", async () => {\n await simulateQueryLLM(\"hello\", {\n provider: \"openai\",\n baseUrl: \"https://oa.api2d.net/v1\",\n apiKey: \"fk-proxy-key\",\n model: \"gpt-4o\",\n });\n\n expect(capturedUrl).toBe(\"https://oa.api2d.net/v1/chat/completions\");\n expect(capturedHeaders.Authorization).toBe(\"Bearer fk-proxy-key\");\n expect(capturedBody.model).toBe(\"gpt-4o\");\n });\n\n it(\"should use proxy URL for volcengine with custom relay site\", async () => {\n await simulateQueryLLM(\"你好\", {\n provider: \"volcengine\",\n baseUrl: \"https://my-proxy.com/volcengine/v3\",\n apiKey: \"ark-proxy-key\",\n model: \"doubao-seed-1-6-251015\",\n });\n\n expect(capturedUrl).toBe(\n \"https://my-proxy.com/volcengine/v3/chat/completions\",\n );\n expect(capturedHeaders.Authorization).toBe(\"Bearer ark-proxy-key\");\n expect(capturedBody.model).toBe(\"doubao-seed-1-6-251015\");\n });\n\n it(\"should use proxy URL for deepseek via OpenAI-SB relay\", async () => {\n await simulateQueryLLM(\"test\", {\n provider: \"deepseek\",\n baseUrl: \"https://api.openai-sb.com/v1\",\n apiKey: \"sb-xxxxxx\",\n model: \"deepseek-chat\",\n });\n\n expect(capturedUrl).toBe(\"https://api.openai-sb.com/v1/chat/completions\");\n expect(capturedHeaders.Authorization).toBe(\"Bearer sb-xxxxxx\");\n });\n\n it(\"should fall back to provider default URL when no base-url is given\", async () => {\n process.env.OPENAI_API_KEY = \"test-key\";\n try {\n await simulateQueryLLM(\"test\", {\n provider: \"openai\",\n model: \"gpt-4o-mini\",\n });\n\n expect(capturedUrl).toBe(\"https://api.openai.com/v1/chat/completions\");\n } finally {\n delete process.env.OPENAI_API_KEY;\n }\n });\n\n it(\"should use proxy URL for One-API gateway\", async () => {\n await simulateQueryLLM(\"test\", {\n provider: \"openai\",\n baseUrl: \"http://localhost:3000/v1\",\n apiKey: \"sk-oneapi-xxxxxx\",\n model: \"gpt-4o\",\n });\n\n expect(capturedUrl).toBe(\"http://localhost:3000/v1/chat/completions\");\n expect(capturedHeaders.Authorization).toBe(\"Bearer sk-oneapi-xxxxxx\");\n });\n\n it(\"should use proxy URL for enterprise Nginx reverse proxy\", async () => {\n await simulateQueryLLM(\"test\", {\n provider: \"openai\",\n baseUrl: \"https://ai-gateway.company.com/v1\",\n apiKey: \"sk-enterprise-key\",\n model: \"gpt-4o\",\n });\n\n expect(capturedUrl).toBe(\n \"https://ai-gateway.company.com/v1/chat/completions\",\n );\n });\n\n it(\"should use Claude model through proxy with openai provider\", async () => {\n await simulateQueryLLM(\"test\", {\n provider: \"openai\",\n baseUrl: \"https://claude-proxy.com/v1\",\n apiKey: \"proxy-key\",\n model: \"claude-sonnet-4-20250514\",\n });\n\n expect(capturedUrl).toBe(\"https://claude-proxy.com/v1/chat/completions\");\n expect(capturedBody.model).toBe(\"claude-sonnet-4-20250514\");\n });\n });\n\n // ─── chat-repl flow with base-url override ─────────────────\n\n describe(\"chat-repl with proxy base-url\", () => {\n /**\n * Simulates the chat-repl URL resolution logic\n */\n function resolveChatUrl(provider, baseUrl) {\n const providerDef = BUILT_IN_PROVIDERS[provider];\n return baseUrl !== \"http://localhost:11434\"\n ? baseUrl\n : providerDef?.baseUrl || \"https://api.openai.com/v1\";\n }\n\n function resolveApiKey(provider, apiKey) {\n if (apiKey) return apiKey;\n const providerDef = BUILT_IN_PROVIDERS[provider];\n return providerDef?.apiKeyEnv ? process.env[providerDef.apiKeyEnv] : null;\n }\n\n it(\"should resolve proxy URL correctly for openai via relay site\", () => {\n const url = resolveChatUrl(\"openai\", \"https://oa.api2d.net/v1\");\n expect(url).toBe(\"https://oa.api2d.net/v1\");\n });\n\n it(\"should resolve proxy URL for volcengine via relay site\", () => {\n const url = resolveChatUrl(\n \"volcengine\",\n \"https://my-proxy.com/volcengine/v3\",\n );\n expect(url).toBe(\"https://my-proxy.com/volcengine/v3\");\n });\n\n it(\"should fall back to volcengine default URL when no proxy is set\", () => {\n const url = resolveChatUrl(\"volcengine\", \"http://localhost:11434\");\n expect(url).toBe(\"https://ark.cn-beijing.volces.com/api/v3\");\n });\n\n it(\"should resolve apiKey from --api-key over env var\", () => {\n process.env.VOLCENGINE_API_KEY = \"env-key\";\n try {\n const key = resolveApiKey(\"volcengine\", \"explicit-key\");\n expect(key).toBe(\"explicit-key\");\n } finally {\n delete process.env.VOLCENGINE_API_KEY;\n }\n });\n\n it(\"should resolve apiKey from env var when --api-key is null\", () => {\n process.env.VOLCENGINE_API_KEY = \"env-key\";\n try {\n const key = resolveApiKey(\"volcengine\", null);\n expect(key).toBe(\"env-key\");\n } finally {\n delete process.env.VOLCENGINE_API_KEY;\n }\n });\n\n it(\"should NOT use OPENAI_API_KEY for volcengine when apiKey is null\", () => {\n process.env.OPENAI_API_KEY = \"openai-key\";\n try {\n const key = resolveApiKey(\"volcengine\", null);\n // Should NOT return the openai key — this was the bug we fixed\n expect(key).not.toBe(\"openai-key\");\n expect(key).toBeFalsy();\n } finally {\n delete process.env.OPENAI_API_KEY;\n }\n });\n });\n\n // ─── agent-repl flow with base-url override ────────────────\n\n describe(\"agent-repl chatWithTools proxy URL resolution\", () => {\n /**\n * Simulates the agent-repl chatWithTools URL resolution\n */\n function resolveAgentUrl(provider, baseUrl) {\n const providerUrls = {\n openai: \"https://api.openai.com/v1\",\n deepseek: \"https://api.deepseek.com/v1\",\n dashscope: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n mistral: \"https://api.mistral.ai/v1\",\n gemini: \"https://generativelanguage.googleapis.com/v1beta/openai\",\n volcengine: \"https://ark.cn-beijing.volces.com/api/v3\",\n kimi: \"https://api.moonshot.cn/v1\",\n minimax: \"https://api.minimax.chat/v1\",\n };\n\n return baseUrl && baseUrl !== \"http://localhost:11434\"\n ? baseUrl\n : providerUrls[provider];\n }\n\n function resolveAgentApiKey(provider, apiKey) {\n if (apiKey) return apiKey;\n const envKeys = {\n openai: \"OPENAI_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n dashscope: \"DASHSCOPE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n gemini: \"GEMINI_API_KEY\",\n volcengine: \"VOLCENGINE_API_KEY\",\n kimi: \"MOONSHOT_API_KEY\",\n minimax: \"MINIMAX_API_KEY\",\n };\n const envKey = envKeys[provider] || \"OPENAI_API_KEY\";\n return process.env[envKey] || null;\n }\n\n it(\"should use proxy URL for all OpenAI-compatible providers\", () => {\n const providers = [\n \"openai\",\n \"deepseek\",\n \"dashscope\",\n \"mistral\",\n \"volcengine\",\n \"kimi\",\n \"minimax\",\n ];\n const proxyUrl = \"https://my-one-api-gateway.com/v1\";\n\n for (const p of providers) {\n const url = resolveAgentUrl(p, proxyUrl);\n expect(url).toBe(proxyUrl);\n }\n });\n\n it(\"should fall back to volcengine default when baseUrl is ollama default\", () => {\n const url = resolveAgentUrl(\"volcengine\", \"http://localhost:11434\");\n expect(url).toBe(\"https://ark.cn-beijing.volces.com/api/v3\");\n });\n\n it(\"should fall back to volcengine default when baseUrl is null\", () => {\n const url = resolveAgentUrl(\"volcengine\", null);\n expect(url).toBe(\"https://ark.cn-beijing.volces.com/api/v3\");\n });\n\n it(\"should resolve volcengine API key from env\", () => {\n process.env.VOLCENGINE_API_KEY = \"ark-test\";\n try {\n const key = resolveAgentApiKey(\"volcengine\", null);\n expect(key).toBe(\"ark-test\");\n } finally {\n delete process.env.VOLCENGINE_API_KEY;\n }\n });\n\n it(\"should prefer explicit apiKey over env var in agent mode\", () => {\n process.env.VOLCENGINE_API_KEY = \"env-key\";\n try {\n const key = resolveAgentApiKey(\"volcengine\", \"explicit-key\");\n expect(key).toBe(\"explicit-key\");\n } finally {\n delete process.env.VOLCENGINE_API_KEY;\n }\n });\n\n it(\"should build correct proxy request to relay site\", async () => {\n const proxyUrl = \"https://oa.api2d.net/v1\";\n const proxyKey = \"fk-relay-key\";\n const model = \"gpt-4o\";\n\n await fetch(`${proxyUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${proxyKey}`,\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"user\", content: \"hello\" }],\n tools: [{ type: \"function\", function: { name: \"read_file\" } }],\n }),\n });\n\n expect(capturedUrl).toBe(\"https://oa.api2d.net/v1/chat/completions\");\n expect(capturedHeaders.Authorization).toBe(\"Bearer fk-relay-key\");\n expect(capturedBody.tools).toBeDefined();\n expect(capturedBody.tools[0].function.name).toBe(\"read_file\");\n });\n });\n\n // ─── Custom provider with proxy URL ─────────────────────────\n\n describe(\"Custom provider registered with proxy URL\", () => {\n it(\"should register and test a custom provider with proxy base URL\", async () => {\n process.env.MY_PROXY_KEY = \"proxy-test-key\";\n try {\n const db = new MockDatabase();\n const registry = new LLMProviderRegistry(db);\n\n registry.addProvider(\"my-proxy\", {\n displayName: \"My OpenAI Proxy\",\n baseUrl: \"https://my-proxy.com/v1\",\n apiKeyEnv: \"MY_PROXY_KEY\",\n models: [\"gpt-4o\", \"claude-sonnet-4-20250514\"],\n });\n\n const provider = registry.get(\"my-proxy\");\n expect(provider).toBeTruthy();\n expect(provider.baseUrl).toBe(\"https://my-proxy.com/v1\");\n\n const result = await registry.testProvider(\"my-proxy\");\n expect(result.ok).toBe(true);\n expect(capturedUrl).toContain(\"my-proxy.com/v1\");\n } finally {\n delete process.env.MY_PROXY_KEY;\n }\n });\n\n it(\"should use proxy key from env for custom provider\", async () => {\n process.env.RELAY_KEY = \"relay-api-key\";\n try {\n const db = new MockDatabase();\n const registry = new LLMProviderRegistry(db);\n\n registry.addProvider(\"relay-site\", {\n displayName: \"Relay Site\",\n baseUrl: \"https://oa.api2d.net/v1\",\n apiKeyEnv: \"RELAY_KEY\",\n models: [\"gpt-4o\"],\n });\n\n const result = await registry.testProvider(\"relay-site\");\n expect(result.ok).toBe(true);\n expect(capturedHeaders.Authorization).toBe(\"Bearer relay-api-key\");\n } finally {\n delete process.env.RELAY_KEY;\n }\n });\n });\n\n // ─── Kimi and MiniMax proxy support ────────────────────────\n\n describe(\"Kimi and MiniMax proxy base-url override\", () => {\n it(\"should use proxy URL for kimi via relay site\", async () => {\n await fetch(\"https://my-proxy.com/moonshot/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer proxy-kimi-key\",\n },\n body: JSON.stringify({\n model: \"moonshot-v1-auto\",\n messages: [{ role: \"user\", content: \"hello\" }],\n }),\n });\n\n expect(capturedUrl).toBe(\n \"https://my-proxy.com/moonshot/v1/chat/completions\",\n );\n expect(capturedHeaders.Authorization).toBe(\"Bearer proxy-kimi-key\");\n expect(capturedBody.model).toBe(\"moonshot-v1-auto\");\n });\n\n it(\"should use proxy URL for minimax via relay site\", async () => {\n await fetch(\"https://my-proxy.com/minimax/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer proxy-minimax-key\",\n },\n body: JSON.stringify({\n model: \"MiniMax-Text-01\",\n messages: [{ role: \"user\", content: \"hello\" }],\n }),\n });\n\n expect(capturedUrl).toBe(\n \"https://my-proxy.com/minimax/v1/chat/completions\",\n );\n expect(capturedBody.model).toBe(\"MiniMax-Text-01\");\n });\n\n it(\"should fall back to kimi default URL when no proxy\", () => {\n const kimi = BUILT_IN_PROVIDERS.kimi;\n expect(kimi.baseUrl).toBe(\"https://api.moonshot.cn/v1\");\n });\n\n it(\"should fall back to minimax default URL when no proxy\", () => {\n const minimax = BUILT_IN_PROVIDERS.minimax;\n expect(minimax.baseUrl).toBe(\"https://api.minimax.chat/v1\");\n });\n });\n\n // ─── Anthropic and Gemini proxy (中转站) ──────────────────\n\n describe(\"Anthropic and Gemini proxy (中转站) support\", () => {\n it(\"should route anthropic-proxy through relay site with OpenAI format\", async () => {\n const proxyUrl = \"https://claude-relay.example.com/v1\";\n await fetch(`${proxyUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer relay-claude-key\",\n },\n body: JSON.stringify({\n model: \"claude-sonnet-4-6\",\n messages: [{ role: \"user\", content: \"test\" }],\n }),\n });\n\n expect(capturedUrl).toBe(\n \"https://claude-relay.example.com/v1/chat/completions\",\n );\n expect(capturedHeaders.Authorization).toBe(\"Bearer relay-claude-key\");\n expect(capturedBody.model).toBe(\"claude-sonnet-4-6\");\n });\n\n it(\"should route gemini-proxy through relay site with OpenAI format\", async () => {\n const proxyUrl = \"https://gemini-relay.example.com/v1\";\n await fetch(`${proxyUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer relay-gemini-key\",\n },\n body: JSON.stringify({\n model: \"gemini-2.0-flash\",\n messages: [{ role: \"user\", content: \"test\" }],\n }),\n });\n\n expect(capturedUrl).toBe(\n \"https://gemini-relay.example.com/v1/chat/completions\",\n );\n expect(capturedBody.model).toBe(\"gemini-2.0-flash\");\n });\n\n it(\"anthropic-proxy and gemini-proxy should use user-provided base URL\", () => {\n // These providers don't have a built-in entry — they're setup-wizard-only\n // The proxy URL is fully provided by the user during setup\n const { LLM_PROVIDERS } = require(\"../../src/constants.js\");\n expect(LLM_PROVIDERS[\"anthropic-proxy\"].isProxy).toBe(true);\n expect(LLM_PROVIDERS[\"anthropic-proxy\"].defaultBaseUrl).toBe(\"\");\n expect(LLM_PROVIDERS[\"gemini-proxy\"].isProxy).toBe(true);\n expect(LLM_PROVIDERS[\"gemini-proxy\"].defaultBaseUrl).toBe(\"\");\n });\n });\n\n // ─── Relay site URL format validation ──────────────────────\n\n describe(\"Relay site URL format validation\", () => {\n const relaySites = [\n { name: \"API2D\", url: \"https://oa.api2d.net/v1\" },\n { name: \"OpenAI-SB\", url: \"https://api.openai-sb.com/v1\" },\n { name: \"CloseAI\", url: \"https://api.closeai-proxy.xyz/v1\" },\n { name: \"AiHubMix\", url: \"https://aihubmix.com/v1\" },\n { name: \"One-API\", url: \"http://localhost:3000/v1\" },\n { name: \"Self-hosted Nginx\", url: \"https://ai-gateway.company.com/v1\" },\n ];\n\n for (const site of relaySites) {\n it(`${site.name} URL should form valid chat/completions endpoint`, () => {\n const endpoint = `${site.url}/chat/completions`;\n expect(endpoint).toMatch(/\\/v1\\/chat\\/completions$/);\n });\n }\n\n it(\"should correctly append /chat/completions to various URL formats\", () => {\n const urls = [\n \"https://proxy.com/v1\",\n \"https://proxy.com/v1/\",\n \"http://localhost:3000/v1\",\n ];\n\n for (const url of urls) {\n const base = url.replace(/\\/$/, \"\");\n expect(`${base}/chat/completions`).toMatch(/\\/chat\\/completions$/);\n }\n });\n });\n});\n"}}}}