Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/chainlesschain_chainlesschain.json
T

1 line
3.4 MiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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\": \"<bce-v3 token>\",\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>`,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\": \"<bce-v3 token>\" }\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\": \"<bce-v3 token>\",\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>`,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\": \"<bce-v3 token>\" }\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 = <filesDir>/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<String, String> = emptyMap()): Array<String> {\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/<abi>/ 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<String, String> {\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<String, String> = 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/<pkg>/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<String> {\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 = <filesDir>/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<String, String> = emptyMap()): Array<String> {\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/<abi>/ 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<String, String> {\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<String, String> = 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/<pkg>/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<String> {\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 = <filesDir>/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<String, String> = emptyMap()): Array<String> {\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/<abi>/ 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<String, String> = 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/<pkg>/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<String> {\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 = <filesDir>/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<String, String> = emptyMap()): Array<String> {\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/<abi>/ 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<String, String> = 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/<pkg>/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<String> {\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 = <filesDir>/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<String, String> = emptyMap()): Array<String> {\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/<abi>/ 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<String, String> = 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/<pkg>/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<String> {\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 = <filesDir>/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<String, String> = emptyMap()): Array<String> {\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/<abi>/ 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<String, String> = 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<String> {\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 = <filesDir>/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<String, String> = emptyMap()): Array<String> {\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/<abi>/ 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<String> {\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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <bg-id> -n 200\ncc agents stop <bg-id>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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\": [\"<sha256-of-public-spki-der>\"],\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 <hex> \\\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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <bg-id> -n 200\ncc agents stop <bg-id>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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\": [\"<sha256-of-public-spki-der>\"],\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 <hex> \\\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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <bg-id> -n 200\ncc agents stop <bg-id>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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\": [\"<sha256-of-public-spki-der>\"],\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 <hex> \\\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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <bg-id> -n 200\ncc agents stop <bg-id>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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\": [\"<sha256-of-public-spki-der>\"],\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 <hex> \\\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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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/<port>.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\": \"<bearer>\",\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 <token>`;`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 <provider>` 入库。完整安装/协议/排错见用户文档 [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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <model>` 或编辑器的「配置图片识别模型」入口里改。\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见 `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见\n `docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\": \"<bearer>\",\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 <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\":\"<bearer>\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\":\"<bearer>\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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 <auto|dark|light|mono>` 切换并持久化(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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\":\"<bearer>\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\":\"<bearer>\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(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 <ref>`/`--range <A..B>`。跑在当前 `cwd`(跟随 `/cd`)+ 复用本会话 provider/model。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\":\"<bearer>\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer <token>`;`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> `<port>.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 字符、保留末尾)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\":\"<bearer>\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer <token>`;`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> `<port>.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 字符)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须自己决定调工具。\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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 <id>` 杀单个(POSIX 杀进程组 / Win `taskkill /T`)、\n `/tasks kill-all` 全杀。sub-agent 委派仍走 `/sub-agents`(另一注册表)。\n- **REPL `/` 命令 tab 补全** — 行首输入 `/he<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\":\"<bearer>\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer <token>`;`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> `<port>.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 字符)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须自己决定调工具。\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\":\"<bearer>\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer <token>`;`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> `<port>.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 字符)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须自己决定调工具。\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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 <id>`(会话内原地切换:重建消息+换 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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\":\"<bearer>\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer <token>`;`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> `<port>.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 字符)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须自己决定调工具。\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\":\"<bearer>\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer <token>`;`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> `<port>.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 字符)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须自己决定调工具。\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\":\"<bearer>\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer <token>`;`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> `<port>.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 字符)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须自己决定调工具。\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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 `<userData>/.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# 开关格式: <feature>-<YYYY-MM-DD>\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 <id> # 绑定 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 <id>` / `--continue`(恢复最近一次)+ JSONL 持久化。\n\n**`--fork-session`**(配 `--resume`/`--continue`):把被恢复的会话**分叉成一个新 id**,原\ntranscript 原封不动保留,本轮起所有新 turn 写进分叉副本(Claude-Code `--fork-session` 对标)。\n分叉副本带完整历史 + 一条 `[Forked from session <id>]` 标记,故 `--resume <新id>` 能端到端重放\n整条分支。stderr 打印 `Forked session <id> → <新id>`。一个 chokepoint 覆盖 headless / stream-json /\n交互三入口(都读 `options.session`);仅对 JSONL transcript 有效,源会话无落盘记录时打印提示并继续\n原会话(不分叉)。无 `--resume` 时为静默 no-op(新跑本身就是新会话)。\n\n```bash\nchainlesschain agent -p \"走另一种方案\" --resume <id> --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 <n>` 设预算(clamp 到 `max_tokens` 之下)。自适应模型忽略 `--thinking-budget`。\n- 思考决策是单一真相源(agent-core `_anthropicThinkingParams`),按 model 自动选自适应 / legacy / 关闭(如 Haiku)。\n\n### 多模态 / 图像输入(`--image <path>`)\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 <sessionId> # 单会话花费\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/<session>/<id>` 下的\n shadow commit,**捕获时不碰真 index / 工作区**;内容寻址(未改文件零成本)、遵守\n `.gitignore`;还原能准确处理 改/增(删除快照后新建的)/删(重建快照里被删的)。\n- **copy-based(fallback,非 git 仓)** `src/lib/file-checkpoint.js` — 快照你指定的\n **显式路径**到 `~/.chainlesschain/checkpoints/<id>/<sha256>`。\n\n```bash\n# git 仓内:create 拍整工作树(不需路径);非 git 仓:必须给 <paths...>\nchainlesschain checkpoint create [paths...] [--label <l>] # 快照\nchainlesschain checkpoint list # 列出 (最新在前)\nchainlesschain checkpoint show <id> # 清单(文件 + 字节数)\nchainlesschain checkpoint show <id> --diff [--stat] # 与当前对比 (git: patch/--stat;copy: 列表)\nchainlesschain checkpoint restore <id> --dry-run # 预览将变更的文件\nchainlesschain checkpoint restore <id> --force # 还原 (别名 rewind;非交互必须 --force)\nchainlesschain checkpoint delete <id> [--force] # 删一个快照 (别名 rm)\nchainlesschain checkpoint clear [--force] # 删整个 session 的快照\n```\n\n公共选项:`-d, --dir <dir>`(目标目录,默认 `.`)、`-s, --session <id>`(git 引擎的\nref 命名空间,默认 `default`)、`--json`。\n\n语义与安全:\n- `restore` **有破坏性**(覆盖当前文件):非 TTY 无 `--force` 拒绝执行;TTY 弹确认。\n- 还原前**自动给当前态拍 safety checkpoint** → 回滚本身可再回滚\n (`cc checkpoint restore <safetyId>`)。\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 <id>`\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 <tool>)\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/<agent-sessionId>/`;连续工具未改文件时去重(不堆冗余 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/<id>.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 <id> # 详情\nchainlesschain goal kr add <id> \"新 KR\" --target 5 # 加关键结果 (可设数值目标)\nchainlesschain goal kr set <id> <krId> --current 5 # current≥target 自动标 done\nchainlesschain goal progress <id> --pct 60 --note \"过半\" # 手动记进度\nchainlesschain goal link <id> [sessionId] # 把会话挂到目标 (省略=最近会话)\nchainlesschain goal pause|resume|close|abandon <id> # 生命周期\nchainlesschain goal active [--session <id>] # 看哪个目标会绑定到运行\nchainlesschain goal rm <id> [--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 <id> # 显式绑定某目标\nchainlesschain agent -p \"继续推进\" --goal # 不带值=自动解析活跃目标\n```\n\n**解析优先级**(`resolveActiveGoal`):① 显式 `--goal <id>`(任意状态)> ② 挂到当前\n会话的活跃目标 > ③ 唯一一个活跃目标 > ④ 无(多个活跃且未挂会话时返回空,需显式指定)。\n显式 `--goal <id>` 还会把会话 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 <id> --goal-assess\n# 跑完后:◎ goal <id>: advanced (45%) ← 自动写回目标\nchainlesschain goal show <id> # 能看到 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 <id> --output-format stream-json\n# → ...{\"type\":\"compaction\",\"stats\":{\"strategy\":\"dedup\",\"originalMessages\":54,\n# \"compressedMessages\":6,\"saved\":879,...}}...\n```\n\n### `cc compact <session-id>` — 手动压缩存档会话\n\n不进 REPL 直接压缩一个已存会话(脚本化 / resume 前预压缩 / 批量维护)。重建会话 →\n跑压缩器 → 追加 `compact` 事件;`rebuildMessages()` 认最后一条 compact 事件,所以之后\n`cc agent --resume <id>` 自动拿短历史。\n\n```bash\nchainlesschain compact <session-id> # 压缩并持久化\nchainlesschain compact <session-id> --dry-run # 只预览缩减量,不写\nchainlesschain compact <session-id> --json # 机读\nchainlesschain compact <session-id> --model <m> --provider <p> # 自适应阈值按指定模型\nchainlesschain compact <session-id> --max-tokens <n> --max-messages <n> # 硬阈值覆盖\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 <dur>`:`30s` / `5m` / `1.5h` / `500ms`;裸数字按**秒**算(`--every 30` === `30s`)。\n- **停止条件**(每轮跑完后判定,所以至少跑一次):`--max-iterations N` / `--until-exit-zero`\n (某轮退出码 0 就停)/ `--until <regex>`(某轮输出匹配 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 <interval>]]` | 下一轮在 `<interval>` 后跑(如 `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/<id>.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 <id>`,`--json` 里带 `sessionId`。\n- **`--resume <id>`** 回放 `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__<server>__<tool>`,经 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__server__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 <uri>`(见\n`docs/cli/core-phases.md`)。\n\n### Prompts — server 提供的斜杠命令(交互 REPL)\n\nMCP server 还可暴露 **prompt**(参数化的 prompt 模板)。交互 REPL 里它们就是斜杠命令\n`/mcp__<server>__<prompt>`(对标 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 <name>`。\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/<port>.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\":\"<bearer>\",\n \"pid\":12345, \"started_at\":1718000000000 }\n```\n约定:`url` 必须 `127.0.0.1`/`::1`;`transport` 仅 `sse`/`http`(client 当前不支持 `ws`);\n连接注入 `Authorization: Bearer <token>`;`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> `<port>.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 字符)作为 `<ide-context>` 块附进**本轮用户消息**——模型无须自己决定调工具。\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-.../<secret> 形式(见下)\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-.../<secret>` 形式**;只填短 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 [<sessionId>] # 省略=最近一次 headless 会话\nchainlesschain context <id> --json\nchainlesschain context <id> --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 <id|last> [-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<TAB>` 补全注册的 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 <file>`(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>` 把对话\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` 启动时自动把文件式项目约定注入系统提示(`<project-instructions>` 块):\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- `! <cmd>` — 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>`)\n- `# <note>` — 一键记到 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<project>/.claude/settings.json # project(签入)\n<project>/.claude/settings.local.json # 个人覆盖(gitignored)\n--settings <file> # 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 <file>` 合并一份 `.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 <name>`(切换,即时重算系统\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 <url>` 授权一次,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 <url> --scope \"read write\" --client-id <id> # 预注册客户端 / 指定 scope\nchainlesschain mcp login <url> --no-open --port 53682 # 打印授权 URL(不开浏览器)/ 自定义回调端口\nchainlesschain mcp auth [--json] # 列出已存 token(valid/expired/+refresh)\nchainlesschain mcp logout <url> # 删除 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 <m>] [--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 `<type>.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 `<feature>-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 `<type>.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.<lifecycle>\", 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.<id>.* 都进 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<boolean>} 是否成功\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<Object|null>}\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.<id>.* 都进 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<boolean>} 是否成功\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<Object|null>}\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 <id> # 代理模式 + 恢复历史会话\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 <question> [options] # 单次问答\n\n选项:\n --model <name> 模型名称(默认 qwen2:7b)\n --provider <provider> LLM 提供商 (ollama/openai/volcengine/deepseek/\n dashscope/gemini/mistral/anthropic)\n --base-url <url> API 基础地址(中转站/代理)\n --api-key <key> API 密钥(可从环境变量读取)\n --agent 启用代理模式(工具调用)\n --session <id> 恢复历史会话(配合 --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 <id> # 代理模式 + 恢复历史会话\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 <question> [options] # 单次问答\n\n选项:\n --model <name> 模型名称(默认 qwen2:7b)\n --provider <provider> LLM 提供商 (ollama/openai/volcengine/deepseek/\n dashscope/gemini/mistral/anthropic)\n --base-url <url> API 基础地址(中转站/代理)\n --api-key <key> API 密钥(可从环境变量读取)\n --agent 启用代理模式(工具调用)\n --session <id> 恢复历史会话(配合 --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 <name> # LLM 提供商 (ollama/openai/volcengine/deepseek/...)\n--base-url <url> # API 基础 URL(覆盖提供商默认地址)\n--api-key <key> # API 密钥(覆盖环境变量)\n--model <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 <name> # ollama | openai | anthropic | volcengine | deepseek | ...\n--base-url <url> # 覆盖提供商默认地址(用于中转站、自建网关)\n--api-key <key> # 覆盖环境变量中的 API Key\n--model <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 <name> \\\n --display-name \"<name>\" \\\n --base-url <url> \\\n --api-key-env <ENV_VAR_NAME> \\\n --models \"<model-a>,<model-b>\"\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 <base-url>/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 <name> # LLM 提供商 (ollama/openai/volcengine/deepseek/...)\n--base-url <url> # API 基础 URL(覆盖提供商默认地址)\n--api-key <key> # API 密钥(覆盖环境变量)\n--model <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 <name> # ollama | openai | anthropic | volcengine | deepseek | ...\n--base-url <url> # 覆盖提供商默认地址(用于中转站、自建网关)\n--api-key <key> # 覆盖环境变量中的 API Key\n--model <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 <name> \\\n --display-name \"<name>\" \\\n --base-url <url> \\\n --api-key-env <ENV_VAR_NAME> \\\n --models \"<model-a>,<model-b>\"\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 <base-url>/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 <question> [options]\n```\n\n### 参数\n\n| 参数 | 说明 |\n| ------------ | -------------------- |\n| `<question>` | 要提问的问题(必填) |\n\n### 选项\n\n| 选项 | 说明 | 默认值 |\n| ----------------------- | --------------------------------------------------------------------------------- | ---------------------- |\n| `--model <model>` | 模型名称 | `qwen2:7b` |\n| `--provider <provider>` | LLM 提供商 (ollama/openai/volcengine/deepseek/dashscope/gemini/mistral/anthropic) | `ollama` |\n| `--base-url <url>` | API 基础地址(可覆盖为中转站/代理地址) | 根据 provider 自动选择 |\n| `--api-key <key>` | API 密钥 | 从对应环境变量读取 |\n| `--json` | 以 JSON 格式输出结果 | — |\n\n## 配置参考\n\n```bash\nchainlesschain ask <question> [options]\n\n参数:\n <question> 要提问的问题(必填)\n\n选项:\n --model <name> 模型名称(默认: qwen2:7b)\n --provider <provider> LLM 提供商 (ollama/openai/volcengine/deepseek/\n dashscope/gemini/mistral/anthropic) 默认: ollama\n --base-url <url> API 基础地址(可覆盖为中转站/代理地址)\n --api-key <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 <question> [options]\n```\n\n### 参数\n\n| 参数 | 说明 |\n| ------------ | -------------------- |\n| `<question>` | 要提问的问题(必填) |\n\n### 选项\n\n| 选项 | 说明 | 默认值 |\n| ----------------------- | --------------------------------------------------------------------------------- | ---------------------- |\n| `--model <model>` | 模型名称 | `qwen2:7b` |\n| `--provider <provider>` | LLM 提供商 (ollama/openai/volcengine/deepseek/dashscope/gemini/mistral/anthropic) | `ollama` |\n| `--base-url <url>` | API 基础地址(可覆盖为中转站/代理地址) | 根据 provider 自动选择 |\n| `--api-key <key>` | API 密钥 | 从对应环境变量读取 |\n| `--json` | 以 JSON 格式输出结果 | — |\n\n## 配置参考\n\n```bash\nchainlesschain ask <question> [options]\n\n参数:\n <question> 要提问的问题(必填)\n\n选项:\n --model <name> 模型名称(默认: qwen2:7b)\n --provider <provider> LLM 提供商 (ollama/openai/volcengine/deepseek/\n dashscope/gemini/mistral/anthropic) 默认: ollama\n --base-url <url> API 基础地址(可覆盖为中转站/代理地址)\n --api-key <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 <id> # 恢复历史会话\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 <model>` | LLM 模型名称 | `qwen2.5:7b` |\n| `--provider <provider>` | LLM 提供商(ollama/anthropic/openai/deepseek/dashscope/mistral/gemini/volcengine) | `ollama` |\n| `--base-url <url>` | API 基础 URL | `http://localhost:11434` |\n| `--api-key <key>` | API 密钥 | — |\n| `--session <id>` | 恢复历史会话 | — |\n| `--add-dir <dir>` | 额外工作根(可重复) | — |\n| `--mcp-config <file>` | 临时挂载 MCP server,工具 → `mcp__<server>__<tool>` | — |\n| `--no-mcp` | 不自动连 `cc mcp add --auto-connect` 的服务 | (默认连) |\n| `--settings <file>` | 一次性 `.claude/settings.json`:权限规则 + `model`/`env` 覆盖 | — |\n| `--output-style <name>` | 套用命名人格(内置 `explanatory`/`learning` 或 `.claude/output-styles/<name>.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 <id> | --continue # 会话恢复 + 自动压缩\nchainlesschain agent -p \"...\" --resume <id> --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 <file>`):最终回答必须是通过该 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 <path>`,可重复,仅 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 <file>`,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 <n>`,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__<server>__<tool>`。\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 <name> | run <name> \"<task>\" | new <name> [--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 [<sessionId>] [--json] [--model <m>] # 按角色拆 token + 占比 + 余量(/context 对标)\n```\n\n## IDE 桥接 — 自动连接编辑器 + 实时感知\n\n在 VS Code / JetBrains 集成终端里跑 `cc agent` 会**自动连接**编辑器的桥接 MCP server(保留名 `ide`,`--ide` 强制 / `--no-ide` 禁用),并获得两个自动行为:\n\n- **提交时共享选区/打开文件**:每条 prompt 自动附带 `<ide-context>` 块(活跃文件、打开 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 <n>` 把对话截回该轮之前并把原文预填到输入行(改完重发);空闲时**双击 Esc** 调出同一选择器。注意这只回退**对话**,文件状态回退用 `cc checkpoint restore`。\n>\n> **恢复摘要(v0.162.40+)**:`--resume` 重建对话后,REPL 自动打印一段**离线提取式**回顾(轮次统计 + 最后提问/回复预览,零 LLM 调用)—— 秒级回想\"刚才聊到哪了\"。\n>\n> **`!` bash 直通(v0.162.41+)**:`! <cmd>` 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>` 块),模型下一轮可见;Windows 走 `chcp 65001` 防乱码,120s 超时 / 30K 输出截断。\n>\n> **`#` 快捷记忆(v0.162.41+)**:`# <备注>` 一键追加到 git 根的 `cc.md`(`## Notes` 段、最新在上),本会话立即生效、之后的会话经[项目记忆](./cli-init#项目记忆)自动加载。\n>\n> **`/` 命令补全(v0.162.41+)**:行首输入 `/he<TAB>` 补全 REPL 命令(命令 token 输入期间生效,空格后不打扰参数)。\n\n### 基础命令\n\n| 命令 | 说明 |\n| ---------------------- | ------------------------------------------------------------------------------------------------------- |\n| `/exit` | 退出代理 |\n| `/help` | 显示所有命令帮助 |\n| `/model <name>` | 切换/查看模型 |\n| `/provider <p>` | 切换/查看提供商,支持 8 个提供商 (ollama/anthropic/openai/deepseek/dashscope/mistral/gemini/volcengine) |\n| `/clear` | 清空对话历史 |\n| `/compact` | 智能压缩对话(基于重要性评分) |\n| `/rewind [n]` | 列出/回退到历史用户轮次(双击 Esc 同效;文件回退用 `cc checkpoint`) |\n| `! <cmd>` | bash 直通:直接跑 shell,输出进对话上下文 |\n| `# <note>` | 快捷记忆:追加到项目 `cc.md` 的 `## Notes` |\n| `/output-style [name]` | 列出 / 切换 [输出风格](./output-styles) 人格;`none` 清除 |\n\n### Context Engineering 命令\n\n| 命令 | 说明 |\n| ---------------------- | ---------------------------- |\n| `/task <objective>` | 设置当前任务目标 |\n| `/task clear` | 清除任务 |\n| `/session` | 显示当前 Session ID 和消息数 |\n| `/session resume <id>` | 加载历史会话消息 |\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 <file>` | 多视角代码评审 |\n| `/cowork compare <prompt>` | A/B 方案对比 |\n| `/cowork graph` | ASCII 代码知识图谱 |\n| `/cowork decision` | 决策追踪知识库 |\n\n### 自主模式命令\n\n| 命令 | 说明 |\n| -------------- | ------------------------- |\n| `/auto <goal>` | 提交目标,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 <id> # 恢复历史会话\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 <model>` | LLM 模型名称 | `qwen2.5:7b` |\n| `--provider <provider>` | LLM 提供商(ollama/anthropic/openai/deepseek/dashscope/mistral/gemini/volcengine) | `ollama` |\n| `--base-url <url>` | API 基础 URL | `http://localhost:11434` |\n| `--api-key <key>` | API 密钥 | — |\n| `--session <id>` | 恢复历史会话 | — |\n| `--add-dir <dir>` | 额外工作根(可重复) | — |\n| `--mcp-config <file>` | 临时挂载 MCP server,工具 → `mcp__<server>__<tool>` | — |\n| `--no-mcp` | 不自动连 `cc mcp add --auto-connect` 的服务 | (默认连) |\n| `--settings <file>` | 一次性 `.claude/settings.json`:权限规则 + `model`/`env` 覆盖 | — |\n| `--output-style <name>` | 套用命名人格(内置 `explanatory`/`learning` 或 `.claude/output-styles/<name>.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 <id> | --continue # 会话恢复 + 自动压缩\nchainlesschain agent -p \"...\" --resume <id> --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 <file>`):最终回答必须是通过该 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 <path>`,可重复,仅 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 <file>`,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 <n>`,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__<server>__<tool>`。\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 <name> | run <name> \"<task>\" | new <name> [--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 [<sessionId>] [--json] [--model <m>] # 按角色拆 token + 占比 + 余量(/context 对标)\n```\n\n## IDE 桥接 — 自动连接编辑器 + 实时感知\n\n在 VS Code / JetBrains 集成终端里跑 `cc agent` 会**自动连接**编辑器的桥接 MCP server(保留名 `ide`,`--ide` 强制 / `--no-ide` 禁用),并获得两个自动行为:\n\n- **提交时共享选区/打开文件**:每条 prompt 自动附带 `<ide-context>` 块(活跃文件、打开 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 <n>` 把对话截回该轮之前并把原文预填到输入行(改完重发);空闲时**双击 Esc** 调出同一选择器。注意这只回退**对话**,文件状态回退用 `cc checkpoint restore`。\n>\n> **恢复摘要(v0.162.40+)**:`--resume` 重建对话后,REPL 自动打印一段**离线提取式**回顾(轮次统计 + 最后提问/回复预览,零 LLM 调用)—— 秒级回想\"刚才聊到哪了\"。\n>\n> **`!` bash 直通(v0.162.41+)**:`! <cmd>` 直接跑 shell(不经 LLM),输出回灌对话上下文(`<bash-input>/<bash-output>` 块),模型下一轮可见;Windows 走 `chcp 65001` 防乱码,120s 超时 / 30K 输出截断。\n>\n> **`#` 快捷记忆(v0.162.41+)**:`# <备注>` 一键追加到 git 根的 `cc.md`(`## Notes` 段、最新在上),本会话立即生效、之后的会话经[项目记忆](./cli-init#项目记忆)自动加载。\n>\n> **`/` 命令补全(v0.162.41+)**:行首输入 `/he<TAB>` 补全 REPL 命令(命令 token 输入期间生效,空格后不打扰参数)。\n\n### 基础命令\n\n| 命令 | 说明 |\n| ---------------------- | ------------------------------------------------------------------------------------------------------- |\n| `/exit` | 退出代理 |\n| `/help` | 显示所有命令帮助 |\n| `/model <name>` | 切换/查看模型 |\n| `/provider <p>` | 切换/查看提供商,支持 8 个提供商 (ollama/anthropic/openai/deepseek/dashscope/mistral/gemini/volcengine) |\n| `/clear` | 清空对话历史 |\n| `/compact` | 智能压缩对话(基于重要性评分) |\n| `/rewind [n]` | 列出/回退到历史用户轮次(双击 Esc 同效;文件回退用 `cc checkpoint`) |\n| `! <cmd>` | bash 直通:直接跑 shell,输出进对话上下文 |\n| `# <note>` | 快捷记忆:追加到项目 `cc.md` 的 `## Notes` |\n| `/output-style [name]` | 列出 / 切换 [输出风格](./output-styles) 人格;`none` 清除 |\n\n### Context Engineering 命令\n\n| 命令 | 说明 |\n| ---------------------- | ---------------------------- |\n| `/task <objective>` | 设置当前任务目标 |\n| `/task clear` | 清除任务 |\n| `/session` | 显示当前 Session ID 和消息数 |\n| `/session resume <id>` | 加载历史会话消息 |\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 <file>` | 多视角代码评审 |\n| `/cowork compare <prompt>` | A/B 方案对比 |\n| `/cowork graph` | ASCII 代码知识图谱 |\n| `/cowork decision` | 决策追踪知识库 |\n\n### 自主模式命令\n\n| 命令 | 说明 |\n| -------------- | ------------------------- |\n| `/auto <goal>` | 提交目标,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"}}}}