Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/chainlesschain_chainlesschain.json
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
3.9 MiB
Plaintext
Raw Permalink 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"}, "7c97c11ebde17017bea27d852ae316118f1c4b4b": {"t": 1786067401.7482555, "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"}, "e032d3f1c7d2add7d94a0226c53d3498cfcb38bb": {"t": 1786067538.2275705, "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"}, "4b2df9c4cc48e7c22db7a58edd28157dbef0e9b4": {"t": 1786067750.9202394, "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"}, "7e63914254eea06b75f4b64ccd44d1993d3be1e8": {"t": 1786068262.1407459, "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"}, "7c97c11ebde17017bea27d852ae316118f1c4b4b": {"t": 1786067444.9729724, "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"}, "7e63914254eea06b75f4b64ccd44d1993d3be1e8": {"t": 1786068308.0728848, "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"}, "7c97c11ebde17017bea27d852ae316118f1c4b4b": {"t": 1786067451.4863813, "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"}, "7e63914254eea06b75f4b64ccd44d1993d3be1e8": {"t": 1786068309.9462187, "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"}}, "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"}, "7c97c11ebde17017bea27d852ae316118f1c4b4b": {"t": 1786067450.804077, "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"}, "7e63914254eea06b75f4b64ccd44d1993d3be1e8": {"t": 1786068310.1800363, "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"}, "e032d3f1c7d2add7d94a0226c53d3498cfcb38bb": {"t": 1786067541.1502852, "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"}, "7e63914254eea06b75f4b64ccd44d1993d3be1e8": {"t": 1786068360.9255826, "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"}}, "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"}, "9332a21ab3d5e31c0d459514d1021f00973668e2": {"t": 1786067820.495701, "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"}, "7e63914254eea06b75f4b64ccd44d1993d3be1e8": {"t": 1786068473.327972, "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"}}, "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"}, "9332a21ab3d5e31c0d459514d1021f00973668e2": {"t": 1786067871.5010283, "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"}, "7e63914254eea06b75f4b64ccd44d1993d3be1e8": {"t": 1786068488.4458303, "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"}}, "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"}, "9332a21ab3d5e31c0d459514d1021f00973668e2": {"t": 1786067996.4659398, "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"}, "8be13d0fbd253bfaa70d5bf157f81f5503bba8ee": {"t": 1786068159.229831, "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"}, "7e63914254eea06b75f4b64ccd44d1993d3be1e8": {"t": 1786068525.2031422, "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"}, "9332a21ab3d5e31c0d459514d1021f00973668e2": {"t": 1786068119.532766, "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"}, "6be06f44482b022b6a15e9f9c674fdad9edb56a6": {"t": 1786068126.9840696, "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"}, "7e63914254eea06b75f4b64ccd44d1993d3be1e8": {"t": 1786068577.3718634, "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"}}, "docs-site/docs/chainlesschain/personal-data-hub.md": {"7e63914254eea06b75f4b64ccd44d1993d3be1e8": {"t": 1786068553.901684, "n": 142423, "h": "868cf13e18dfb829", "c": "# 个人数据中台 (Personal Data Hub)\n\n> **状态(2026-08-05):92 个采集契约 / 18 类已注册;PDH 0.4.57 保持不变,当前 npm `latest` 为 CLI 0.162.197。92 是能力清单,不代表每个来源都已在当前设备或账号上可用;页面会按真实输入与宿主能力显示“可采集 / 需配置 / 不可用”。**\n>\n> 让数据回归个人。各 App 的数据先落到你自己设备上,本地 LLM 才能用它帮你回答跨源问题。任何分析都不经云端 — 默认拒绝非本地 LLM,除非显式 opt-in。\n\n> 版本演进与各 Adapter 落地节点见下文 [最新更新](#最新更新) 与 [系统架构 → Phase 历史](#phase-历史)。\n\n## 概述\n\n个人数据中台 (Personal Data Hub) 是 ChainlessChain 把\"个人 AI 工具集\"升级为**用户全数据本地中台 + AI 分析引擎**的核心基础设施。它解决一个长期存在的问题:用户的数据散落在数十个 App 里(微信 / 支付宝 / 淘宝 / 高德 / 各 AI 助手 / …),每个 App 是事实主人,用户只是访问者;跨源问题(\"我妈生日那周买了啥送哪儿?\")任何单一 App 都答不了,而云端 AI 看不到也不该看到你的私域数据。\n\n中台的工作方式:\n\n1. **采集**:每个 App 一个 Adapter,把数据拉回本机\n2. **统一**:5 类核心实体(Person / Event / Place / Item / Topic)跨源对齐\n3. **存储**:SQLCipher LocalVault (AES-256) 加密落盘,主密钥绑定平台 Keystore,可选 U-Key / SIMKey 硬件加固\n4. **理解**:派生进既有 KG(知识图谱)+ RAG (BM25) 索引,让数据可被自然语言查询\n5. **分析**(核心):本地 Ollama LLM 直接分析用户数据,敏感数据零外传;隐私 gate 默认拒非本地模型\n6. **可携带**:JSON / SQLite 一键导出,可销毁,全过程审计可查\n\n中台**不新增基础设施** — KG / RAG / SQLCipher / DID / Skill 系统 / Ollama 全部复用既有能力,新增的只是 UnifiedSchema 定义、EntityResolver、AdapterRegistry、每个 Adapter 实现,以及数据分析专用 prompt / skill。\n\n> 完整设计与 13-Phase 路线图见设计文档:[`Personal_Data_Hub_Architecture.md`](https://design.chainlesschain.com/Personal_Data_Hub_Architecture.html)(\"个人数据中台 专题\")。\n\n## 最新更新\n\n> 本节汇总近几个版本的采集能力更新;更细的逐 Phase 落地记录见 [系统架构 → Phase 历史](#phase-历史)。\n\n### 事务化多源归并与完整续扫(CLI 0.162.183 / PDH 0.4.57)\n\n同一条事实可能同时来自 QQ NT 数据库、Python sidecar、Android 备份或其它采集路径。新版不再用“后写覆盖前写”处理这些证据:\n\n- 来源别名、每个 producer 的原始 observation、字段级冲突决策、批内引用重写和最终实体在**同一事务**提交;任一步失败都会整体回滚。\n- 富文本、联系人名、已读状态等字段按质量与新鲜度合并,旧 observation 仍可审计回查;升级前的 QQ 行会迁移到 canonical identity,不重复生成一份实体。\n- KG 与 RAG 只接收 Vault 真正落盘后的 ID 与引用,避免索引指向已被归并掉的临时 ID。\n- 本地数据库、桌面客户端、移动 API、地图、社交、IM、教育、音乐、健康、购物和出行来源统一使用显式游标与有界页预算。\n- 重复页、停滞页、部分读取、不可读 SQLite、取消或超时会明确报错并保留旧 checkpoint;修好输入后再次同步即可从安全位置继续。\n\n对于日常用户,这意味着同一账号从多条路径采集不会静默复制或丢字段,长历史也不会因为单次预算到顶就被误判为“已经采完”。\n\n### 跨端实时采集与可靠性收口(v5.0.3.135 / CLI 0.162.179 / PDH 0.4.55)\n\n桌面端、`cc ui` Web Panel 和 CLI 现在共用同一套实时采集能力。数据源卡片会根据 adapter 契约显示对应入口:**立即采集、选择文件、选择目录、登录 Cookie、OAuth 授权、USB 一键采集**或“查看步骤”,不会再对缺失输入的来源发起注定失败的空同步。\n\n常用操作:\n\n1. 打开“个人数据中台 → 数据源”,先看来源的就绪状态与原因。\n2. 本地/已配置来源点“立即采集”;文件或目录来源只扫描你本次明确选择的范围。\n3. Cookie 与 OAuth 只用于本次采集。百度网盘与 WPS 的 access token / App Key 不写入本地配置、数据中台、审计日志或同步水位,提交后表单立即清空。\n4. Android 来源连接一台已授权设备后使用“USB 一键采集”;多设备时先明确选择 serial,避免采错设备。\n5. 采集中可查看 connecting / fetching / normalizing / done / error 流式状态;取消、超时、来源变化或不完整分页不会推进旧水位,下次可安全续扫。\n\n本轮还修复了 QQ NT 字段语义、Windows Recent 与 Android 系统数据续扫、带密钥 WhatsApp ADB 备份、微信读书游标保留和 Python sidecar 协作取消。无效 Cookie 来源页、缺失备份或不可达输入统一失败闭合,不再显示“成功但 0 条”。\n\n### 本机零配置开发者数据源(Phase 17/18,v5.0.3.83+)\n\n7 个零配置 / no-arg 即采集的本机数据源,对开发者尤其重要:你的本机操作流也是个人 RAG 语料。\n\n| Adapter | 数据源 | 平台 | 内容 | 启用方式 |\n| -------------------------- | ----------------------------------------------------------- | -------- | ------------------------------------------- | -------------------- |\n| **git-activity** | 本机所有 git repo | 桌面三端 | commit / 分支 / merge 历史 | 默认扫描常用代码目录 |\n| **shell-history** | bash / zsh / pwsh history | 桌面三端 | 命令历史,去敏感词 | 默认 |\n| **vscode** | VSCode 用户配置 | 桌面三端 | 最近工作区 + 集成终端历史 | 默认 |\n| **win-recent** | `%APPDATA%/Microsoft/Windows/Recent/*.lnk` | Windows | 跨应用文件打开历史(任何 App 打开过的文件) | Windows 默认 |\n| **local-files** | Documents / Desktop / Downloads / Pictures / Videos / Music | 桌面三端 | 文件元数据遍历(不读内容) | 默认 |\n| **browser-history-chrome** | `Default/History` SQLite | 桌面三端 | 浏览历史 + 书签 | Chrome 安装即扫 |\n| **browser-history-edge** | `Default/History` SQLite | 桌面三端 | 浏览历史 + 书签 | Edge 安装即扫 |\n\n所有 7 个 adapter 走 `category=system` bucket 在 Vault Browser 侧栏可单独看;FTS5 全文检索同样有效。`cc hub sync browser-history-chrome` 单独触发某个 adapter,`cc hub stats --adapter <name>` 看分源覆盖。\n\n### 采集能力大更新(v5.0.3.99)— readiness + 一键采集 + 多家本地直读\n\n这一版把个人数据中台的「采集」体验从「能跑但要手动多步配置 + 经常一条都采不到」推到「就绪可见 + 一键触发」:\n\n- **adapter readiness 止血** — 过去 `healthCheck` 是个宽松的「能不能 sync」闸门,配置看起来正常但实际采不到时没有任何信号。本轮把真正的「就绪」判定独立成 `registry.readiness()`:每个数据源返回 `ready / needs_setup / unavailable` + 一行中文原因,UI 直接显示「为什么现在采不了、下一步该做什么」。\n- **一键采集 / 导入引导 UI** — 把「装 App → 进采集页 → 填授权码 / root + ADB pull DB / IMAP 登录」这些分步操作收敛成单一入口。每个源的具体步骤由 `adapter-guide.js` 单源维护,web-shell / 桌面 / CLI / Android 四壳共用,指引不再各壳漂移。\n\n新接通的本地直读数据源:\n\n| 数据源 | 平台 | 采集方式 | 说明 |\n| ------------------- | --------- | ------------ | ------------------------------------ |\n| **抖音** | 桌面 | 本地直读 | 电脑端本地 DB / 文件直读样板 |\n| **微信(电脑版)** | 桌面 | 本地直读 | PC 端本地数据直读 |\n| **QQ(电脑版 NT)** | 桌面 | 本地直读 | NT 版本地 DB 直读 |\n| **钉钉(电脑版)** | 桌面 | 本地 IM 直读 | honest best-effort 本地 IM 采集 |\n| **飞书(电脑版)** | 桌面 | 本地 IM 直读 | honest best-effort 本地 IM 采集 |\n| **微信读书** | 桌面/移动 | cookie | weread cookie 采集 + 一键登录采集 UI |\n| **Apple 健康** | 移动 | 导出导入 | 健康数据 adapter + 一键采集 UI |\n| **网易云音乐** | 移动 | 本地/cookie | 听歌记录 adapter + 一键采集 UI |\n\n此外:**email 账单 LLM 补全(Phase 5.5)** — 邮件账单解析在结构化字段缺失时走 LLM gap-fill 补齐金额 / 商户 / 时间;**iOS 加密备份解密(Phase 7.5b)** — 移动提取层支持解密 iOS 加密备份后导入。当前 PDH 包共 **51 个 Adapter**、**121 测试文件 / 2040 测试**。\n\n> 注:改动 `packages/personal-data-hub/lib/**` 发版时必同步 bump pdh 包 version + npm publish + Android `USR_VERSION`(否则真机走 fast-path 跳解压用旧代码)。当前公网版本为 PDH 0.4.57 / CLI 0.162.197;Android runtime bundle 仍按独立发布步骤与应用版本同步。\n\n## 核心特性\n\n- 🔐 **本地优先加密落盘**:SQLCipher AES-256 LocalVault,单文件即可备份/迁移;FileKeyProvider 默认 0600 权限存放主密钥,Phase 4 升级为 Windows DPAPI / macOS Keychain / Linux libsecret\n- 🧬 **UnifiedSchema 5 类核心实体**:Person / Event / Place / Item / Topic,所有 Adapter 输出统一形状,KG / RAG / 分析层只看一种 schema\n- 🆔 **UUID v7 时间序 ID**:RFC 9562 手写实现(~30 LOC, 零依赖),事件天然按时间排序,索引友好\n- 🪪 **Adapter 契约**:`PersonalDataAdapter` 接口 + `assertAdapter` 校验,每个 Adapter = 一个 Skill,自然接入既有 marketplace 与权限模型\n- 📧 **EmailAdapter (IMAP) 已上线**:QQ / Gmail / 163 等主流邮箱授权码登录,UID 增量水位,正文 + 附件元数据解析,PDF 自动解密 + 字段提取,6 套模板(账单 / 政务 / 订单 / 注册 / 出行 / 其他),分类器双层(规则 + LLM 兜底)\n- 💸 **AlipayBillAdapter (CSV-import) 已上线**:支付宝官方账单 ZIP 文件直接导入,自动 ZIP 解密(用户提供密码)、CSV 解析、对手方归一化、订单事件持久化\n- 🧠 **AnalysisEngine 自然语言 Q&A**:query-parser 提取时间窗口 / 实体 / 意图 → vault 取相关事实 → RAG 召回扩展上下文 → prompt-builder 构造(system 不放事实 / 用户角色才挂事实并标记 untrusted)→ LLM 推理 → citation 校验 → 写入审计\n- 🛡️ **隐私 Gate (硬约束)**:`AnalysisEngine` 检测到 LLM 非本地 (provider 不在白名单) 时直接拒绝 `ask`,调用方必须显式传 `acceptNonLocal: true` 才允许;Ollama / 自托管 vLLM 始终视为本地\n- 📚 **citation 强制校验**:LLM 必须给出引用 eventId,回答经 `validateCitations` 比对 vault 真实事件 → 防幻觉\n- 🪢 **KG 派生 (CcKgSink)**:UnifiedSchema → `rdf:type` / `by` / `involves` / `happened-at` 三元组,对接既有 `cc knowledge-graph` 引擎(`addEntity` + `addRelation`)\n- 🔍 **RAG 派生 (CcRagSink + BM25)**:events 转 RagDoc 写入既有 BM25 索引;Qdrant 向量检索预留 Phase 4\n- 🌉 **CcLLMAdapter (LLM 桥)**:依赖注入复用桌面端已配好的 LLM Manager (Ollama / Volcengine / Anthropic / Gemini / DeepSeek);Hub 包本身**不**依赖 cc 模块,bridge 在 desktop / cli 入口分别注入,Hub 自身可独立单元测\n- 🔁 **AdapterRegistry 全流水线**:health → sync → archive raw → normalize → partition valid/invalid → vault → KG sink → RAG sink → watermark → audit,单段失败不阻塞整体(坏数据进 invalid 队列),sink 故障容忍\n- 🧩 **事务化多源归并**:来源别名、producer raw observation、字段级冲突决策、引用重写与最终实体一次提交;KG / RAG 只消费 canonical 持久 ID\n- ⏬ **增量同步 watermark**:每个 (adapter, partition) 一个水位,重跑同窗口 0 重复事件;`findBySource(adapter, originalId)` 提供去重原语\n- 🧭 **显式游标与完整续扫**:长历史使用版本化 cursor + 有界页预算;重复/停滞页、部分读取、取消与不可读数据库失败闭合且不推进 checkpoint\n- 🌊 **流式同步进度 (Phase 5.7)**:`sync-adapter-stream` / `sync-all-stream` 主题推送 connecting / fetching / normalizing / done / error 事件,UI 实时进度条\n- 🔄 **重试退避 (Phase 5.7)**:网络瞬断自动 3 次指数退避,IMAP 连接 / Alipay 解压都受益\n- 🧾 **审计日志**:每一次 ingest / query / ask / register / unregister 都落 `audit_log` 表,含来源 adapter / 事件 id / 时间戳 / action / 操作者,支持 `queryAudit` 反查\n- 🧊 **冷启动延迟懒加载**:`getHub()` 首次 IPC 才初始化 vault + LLM + 桥;不用 hub 功能的用户零开销\n- 🪟 **跨壳一致接口**:同一组主题在 Electron IPC (colon style `personal-data-hub:ask`) 与 cc ui WS (dot style `personal-data-hub.ask`) 双登记 — SPA 切换 shell 行为不变(per `feedback_cross_shell_feature_pattern`)\n- 💣 **可销毁 (vault.destroy)**:单调用即可彻底清理 vault.db + WAL + 关联密钥,符合\"数据可走\"承诺\n- 🔁 **WAL 安全的主密钥旋转**:`vault.rotateKey(newKeyHex)` 在 WAL 模式下也能透明换密钥,旧密钥保留为 `vault:<id>:prev` 应急回滚\n- 🧪 **Validator 永不抛**:所有校验器返回 `{ valid, errors }`,单条坏数据不会拉垮整窗 ingest,可定向回看\n- 🧰 **MockAdapter / OllamaClient 自带**:测试用 deterministic MockAdapter (seeded)、单机 fallback OllamaClient (`http://localhost:11434` 可改 env)\n- 🕵️ **Root 设备端侧取证采集(v0.7)**:rooted 设备上绕开 cookie/签名风控,直取已登录加密 app 的本地库——**方法 B** 免密钥 `/proc/<pid>/mem` 内存扫描(引擎无关、扛反调试、无需登录口令)/ **方法 C** frida `sqlcipher_export` 在线解密 IM / SQLite 叶子页 **salvager**(`--unaligned`,从损坏内存 dump 救出明文页)/ D1/D2 root 采集 + 多 app 并采 + 来源归属。仅限**用户本人设备 / 本人账号 / 本人 app**;运维细节见仓库内部 runbook `docs/internal/pdh-db-decryption-runbook.md`\n- 📊 **跨 app 统一概览(analysis.overview)**:`ask({crossApp})` 把跨 app 数据总览注入 AnalysisEngine prompt 做决策支持;分析管线去噪(话题名分组 / 兴趣过滤 / timeline inventory 排除)减少噪声\n\n## 系统架构\n\n### 分层视图\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ 用户查询层(自然语言) │\n│ 桌面 PersonalDataHub.vue (Electron / V6) │\n│ Web-Shell PersonalDataHub.vue (cc ui via WS) │\n│ 移动端 Phase 14 路线图(远程操控 RPC,hub.ask 走 P2P DC) │\n│ Q: \"上个月我妈生日那周买了啥送哪儿?\" │\n└─────────────────┬───────────────────────────────────────────┘\n ↓\n┌─────────────────────────────────────────────────────────────┐\n│ 分析层 (AnalysisEngine) │\n│ query-parser → vault facts → RAG retriever (BM25 topK=10) │\n│ → prompt-builder → llm.chat → citation parser/validator │\n│ → audit │\n│ 本地 LLM (Ollama 默认 qwen2.5:7b-instruct, 可切 70B / vLLM) │\n│ 隐私 Gate: 非本地 LLM 默认拒绝 │\n└─────────────────┬───────────────────────────────────────────┘\n ↓ structured query\n┌─────────────────────────────────────────────────────────────┐\n│ 中台核心 (Hub Core) │\n│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │\n│ │ UnifiedSchema│ │ KG Derivation│ │ RAG Derivation│ │\n│ │ Person/Event │ │ → triples │ │ → RagDoc │ │\n│ │ Place/Item │ │ │ │ │ │\n│ │ Topic │ │ │ │ │ │\n│ └──────────────┘ └──────┬───────┘ └──────┬────────┘ │\n│ ┌────────────────────────┼────────────────────┼─────────┐ │\n│ │ LocalVault (SQLCipher AES-256) │ │ │\n│ │ events / persons / places / items / topics │ → CcKgSink │\n│ │ raw_events / sync_watermarks / audit_log │ CcRagSink │\n│ │ 主密钥: FileKeyProvider (Phase 3.5b) │ │\n│ │ → DPAPI / Keychain / libsecret (P4) │ │\n│ └────────────────────────┬────────────────────┴─────────────┘\n└─────────────────────────────┼────────────────────────────────┘\n ↓ Adapter contract\n┌─────────────────────────────────────────────────────────────┐\n│ 同步层 (AdapterRegistry) │\n│ health → sync → archive raw → normalize → partition → │\n│ vault.putBatch → kgSink.write → ragSink.write → watermark │\n│ → audit │\n│ 流式进度 (Phase 5.7) + 指数退避重试 (Phase 5.7) │\n└─────────────────────────┬───────────────────────────────────┘\n ↓\n┌─────────────────────────────────────────────────────────────┐\n│ Adapter 层 (16 个,每个 App 一个 Skill) │\n│ │\n│ Web API / Cookie 类: │\n│ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │\n│ │ EmailIMAP ✅│ │ AlipayBill ✅│ │ AIChat × 8 ✅│ │\n│ │ (P5.1-5.7) │ │ (P6.1-6.5) │ │ (P10.2 8/8) │ │\n│ └─────────────┘ └─────────────┘ └──────────────┘ │\n│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │\n│ │ Taobao ✅ │ │ JD ✅ │ │ Meituan ✅ │ │\n│ │ (P7 Cookie) │ │ (P7 Cookie) │ │ (P7 Cookie) │ │\n│ └──────────────┘ └──────────────┘ └──────────────┘ │\n│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │\n│ │ Amap ✅ │ │ Baidu Map ✅ │ │ Ctrip ✅ │ │\n│ │ (P9 Cookie) │ │ (P9 Cookie) │ │ (P9 Cookie) │ │\n│ └──────────────┘ └──────────────┘ └──────────────┘ │\n│ ┌──────────────┐ │\n│ │ 12306 ✅ │ │\n│ │ (P9 web) │ │\n│ └──────────────┘ │\n│ │\n│ 本机 / 移动端 SQLite 类(借 sjqz parser 复用): │\n│ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ │\n│ │ SystemData ✅│ │ Mobile Ext ✅│ │ WeChat 🚧0.5 │ │\n│ │ (P4.5 sidecar)│(P7.5 ADB/iOS)│ │ (P12 frida- │ │\n│ │ │ │ │ │ indep slice)│ │\n│ └──────────────┘ └──────────────┘ └──────────────┘ │\n│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │\n│ │ Bilibili ✅ │ │ Weibo ✅ │ │ Douyin ✅ │ │\n│ │ (P13.1) │ │ (P13.2) │ │ (P13.3) │ │\n│ └──────────────┘ └──────────────┘ └──────────────┘ │\n│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │\n│ │ Xiaohongshu ✅│ │ QQ ✅ │ │ Telegram ✅ │ │\n│ │ (P13.4) │ │ (P13.5) │ │ (P13.6) │ │\n│ └──────────────┘ └──────────────┘ └──────────────┘ │\n│ ┌──────────────┐ │\n│ │ WhatsApp ✅ │ │\n│ │ (P13.7) │ │\n│ └──────────────┘ │\n└─────────────────────────────────────────────────────────────┘\n\n ✅ 已上线 🚧 部分实现(v0.5 frida-indep 路径已通,frida-dep 路径与真机 E2E 待)\n```\n\n### 5 类核心实体\n\n所有 Adapter 把原始数据 normalize 成下面 5 类。每类继承 `BaseEntity`(`id` / `source` / `ingestedAt` / `confidence?` / `extra?`)。\n\n| 类型 | 子类 (subtype) | 示例 |\n| ---------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |\n| **Person** | self / contact / merchant / ai-agent / unknown | 自己 / 妈妈 / 京东商家 / DeepSeek 助手 |\n| **Event** | message / order / payment / visit / post / browse / like / trip / call / media / ai-message / ai-image-generation / other | 微信聊天 / 淘宝订单 / 支付宝交易 / 餐厅消费 / 朋友圈 / 短视频点赞 / AI 对话 |\n| **Place** | home / restaurant / office / merchant-physical / generic | 家 / 海底捞中山公园店 / 妈妈家 |\n| **Item** | product / link / media / document / generic | 蛋白粉 / 微信链接 / 图片 / PDF 账单 |\n| **Topic** | (free-form 标签) | \"妈妈生日\" / \"Python 学习\" / \"和 GPT 聊编程\" |\n\n> 设计原则:核心 5 类强 schema (跨源对齐用),子类型只是约束 `extra` 字段语义;新数据源不在 5 类里的字段全挂 `extra: Record<string, any>` 兜底,schema 演化不破坏既有数据。\n\n### 已落地 vs 路线图\n\n> 截至 2026-07-23:路线图 adapter 已从 placeholder/scaffold 升级为可执行的 snapshot、SQLite、ADB 或 cookie 采集路径;“代码路径已接通”不等于“当前线上端点已用真账号验收”。v1 sign-off 仍需在用户本人设备和账号上验证 AIChat 私有接口字段、头条/快手等动态签名命中率、WhatsApp 真机备份 UX,以及微信等 root 路径。\n\n| Adapter | 状态 | 数据源 | 采集机制 | 验证 |\n| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| **EmailAdapter** | ✅ 已上线 (Phase 5.1-5.7) | IMAP 邮箱(QQ / Gmail / 163 / …) | IMAP 授权码 + UID 水位 + PDF 解密 + 6 模板 | 单测 ≥390 / E2E 真账号 OK |\n| **AlipayBillAdapter** | ✅ 已上线 (Phase 6.1-6.5) | 支付宝官方账单 ZIP | 用户在 App 内导出 → 拖入桌面 / Web → ZIP 解密 + CSV 解析 + 对手方归一化 | smoke test green |\n| **SystemDataAdapter** | ✅ 已上线 (Phase 4.5) | Android 通讯录 / 通话 / 短信 / WiFi | Python sidecar (forensics-bridge) + adb pull | 单测 50+ |\n| **Mobile Extraction Layer** | ✅ 已上线 (Phase 7.5) | Android ADB + iOS iTunes backup 提取层 | adb / pymobiledevice3 | 单测 + 真机 E2E |\n| **EntityResolver** | ✅ 已上线 (Phase 8) | 跨源 Person / Place 消歧 | 三段 pipeline(规则 + embedding + LLM)+ ingest hook + UI 队列 | 单测全绿 |\n| **5 Analysis Skills** | ✅ 已上线 (Phase 11) | spending / relations / footprint / interests / timeline | 内置 skill + Workflow | 单测全绿 |\n| **TaobaoAdapter** | ✅ 已上线 (Phase 7) | 淘宝订单 / 收藏 / 评价 / 收货地址 | Cookie + web API(共享 `shopping-base`) | 单测全绿 |\n| **JDAdapter** | ✅ v0.2 dual-mode (snapshot + cookie) (`f3bd0693`) | 京东订单 / 收藏 / 评价 | Android in-app snapshot 或桌面 Cookie + web API | 单测全绿(snapshot 8 + 既有) |\n| **MeituanAdapter** | ✅ v0.2 dual-mode (`f3bd0693`) | 美团订单 / 外卖 / 收藏 | Android in-app snapshot 或桌面 Cookie + web API | 单测全绿 |\n| **PinduoduoAdapter** | ✅ v0.2 (`78695c25e`) | 拼多多订单 / 收藏 | Android in-app SAF JSON import | snapshot 8 单测 |\n| **AmapAdapter** | ✅ v0.2 cookie-scrape WebView (`0fe572f2`) | 高德足迹 / 收藏地点 | Android in-app WebView cookie scrape 或桌面 Cookie | 单测全绿 |\n| **BaiduMapAdapter** | ✅ v0.2 (`3d1cf9481`) | 百度地图足迹 / 收藏 | Android in-app WebView 或桌面 Cookie + map.baidu.com API | snapshot 8 单测 |\n| **TencentMapAdapter** | ✅ v0.2 (`3d1cf9481`) | 腾讯地图足迹 / 收藏 | Android in-app WebView cookie scrape | snapshot 8 单测 |\n| **CtripAdapter** | ✅ v0.2 cookie-scrape (`0fe572f2`) | 携程酒店 / 机票 / 火车票订单 | Android in-app WebView 或桌面 Cookie | 单测全绿 |\n| **12306Adapter** | ✅ 已上线 (Phase 9) | 12306 行程 / 订单 | 被动 webRequest 拦 + JSON 解析 | 单测全绿 |\n| **AIChatHistoryAdapter × 9** | ✅ v0.2 全 9 路(豆包 Doubao 已 v0.2 接通) | DeepSeek / Kimi / 通义千问 / 智谱清言 / 腾讯混元 / 百度千帆 (合并 wenxin/qianfan) / 字节扣子 / 即梦 Dreamina / **豆包 Doubao** | Android in-app WebView cookie scrape + cc sync wire (`1e7725552`) 或桌面 HttpClient (cookie + rate-limit + 指数退避) | 全绿 |\n| **Social × 6** (Bilibili / Weibo / Douyin / Xiaohongshu / Toutiao / Kuaishou) | ✅ × 6 采集路径已接通;真账号按平台验收 | Android 本机 SQLite (dual-mode snapshot + sqlite) 或桌面 cookie web API:B 站 history / 微博 timeline (`c087c36eb`) / 抖音 watch (`20f9b2188`) / 小红书 收藏 (`20f9b2188`) / 头条 read_history (`e1155b1d7`) / 快手 photo_history (`e1155b1d7`) | Mobile Extraction Layer pull / Android in-app collector / sjqz parser 移植 | snapshot/sqlite/ADB 结构回归已覆盖;动态签名和新版加密库仍以真机 E2E 为发布闸门 |\n| **Messaging × 3** (QQ / Telegram / WhatsApp) | ✅ 三家 Android 本地采集 | QQ Android **普通 SQLite + msgData XOR(IMEI) 加密**;Telegram `cache4.db`;WhatsApp 可由桌面/CLI ADB 从公共备份目录自动拉取(无需 root),或导入明文 `msgstore.db` / 用户自有 key 的 crypt14 / crypt15,并兼容新旧消息表、媒体、位置、名片、引用与通话 | QQ XOR 解密与 dual-mode adapter;WhatsApp ADB 路径白名单 + 流式 AES-GCM 认证解密 + zlib + SQLite 校验,拉取副本与临时明文自动清理 | WhatsApp 32 单测 + 上游真实 crypt14 / crypt15 / msgstore fixture 交叉验证;QQ Kotlin 27 unit + JS snapshot 13 + longtail 6 |\n| **WechatAdapter** | ✅ **v0.2 Phase 12.10 4 sub-phase 全 land** (8081f8a0d / 37a4e465d / cdfe1048e) | Android in-app SQLCipher 真解密:7.x MD5(IMEI+UIN)[:7] 路径 + 8.x frida hook libwcdb 拿真 64-hex 路径双通路;WAL+SHM cohort copy;vendored frida-inject 16.5.9 arm64+armeabi-v7a APK ship | content-parser + KNOWN_PRAGMA_PROFILES 3 fallback + sjqz wechat_decrypt.py 算法 byte-identical 移植 + 5-symbol frida hook (sqlite3_key/v2/wcdb_setkey/WCDBKeyDerive/mangled C++) | 89 既有 + Phase 12.10 51 新(CredentialsStore 14 + DbExtractor 17 + FridaInjector 15 + LocalCollector 10)+ APK ship 真机已上 Xiaomi 24115RA8EC;剩 Phase 12.10.6 真机 E2E (需 root 机子) + Phase 12.10.7 anti-detection |\n\n> Phase 13 七个 adapter 借 sjqz 已有 Python parser 移植到 Node.js — 详见 [`Adapter_Social_Messaging.md`](https://design.chainlesschain.com/Adapter_Social_Messaging.html)(本批最新加入)。Toutiao §11 + Kuaishou §12 已从 v0.1 scaffold 升 v0.2 dual-mode (`e1155b1d7`, 2026-05-22);Doubao AIChat 9th vendor §6.9 也接通;微博 §13 v0.2 镜像 Bilibili pattern (`c087c36eb`, 27 新单测)。\n\n> **v0.2 升级路径**:所有 v0.2 adapter 都是 dual-mode — 既支持桌面侧既有 cookie + web API 通路(解决用户不在 Android 端的场景),也支持 Android in-app collector 直接采本机 SQLite (绕开网络 / 反爬 / 频控)。Android 路径见 §A8(社交内容)/§2.4-§2.6(购物/出行/AI 助手)的 in-app 卡 UI,桌面路径见各 adapter 的 cookie-scrape WebView。\n\n### Phase 历史\n\n| Phase | 主题 | 关键产出 | 测试基线 |\n| -------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| 0 | UnifiedSchema | 5 类实体 + validators + UUID v7 + batch helpers | 53 |\n| 1 | LocalVault | SQLCipher + 8 张表 migrations + FileKeyProvider | 101 |\n| 2 | AdapterRegistry | 全流水线 + KG/RAG derivation + MockAdapter | 157(1k events @ 600ms) |\n| 3 | AnalysisEngine | NL Q&A + 隐私 gate + citation 校验 | 220 |\n| 3.5 | 桥接生产 | CcLLMAdapter + CcKgSink + CcRagSink (DI) | 268 |\n| 3.5b | 跨壳接线 | 21 IPC 通道 + 21 WS 主题 | — |\n| 4.5 | SystemDataAdapter | Python sidecar + 通讯录 / 通话 / 短信 / WiFi | 累计中 |\n| 5.1 | EmailAdapter | IMAP + 授权码 + UID 水位 | 317 |\n| 5.2 | Email parse | 正文解析 + 附件元数据 | 345 |\n| 5.3 | Email classifier | 规则 (Layer 1) + LLM (Layer 2) 兜底 | 390 |\n| 5.4 | Field extractors | 6 套模板(账单/政务/订单/注册/出行/其他) | 累计中 |\n| 5.5 | PDF 解密 + 提取 | 自动解密含密 PDF + 交易抽取 | 累计中 |\n| 5.6 | Email 配置向导 | 测试授权 + 注册 + 事件详情 deep-link | 累计中 |\n| 5.7 | 流式同步 | 进度推送 + 指数退避重试 + E2E Runbook | 累计中 |\n| 6.1+6.2 | AlipayBillAdapter | CSV 解析 + ZIP 解密 + 对手方归一化 | 累计中 |\n| 6.3-6.5 | Alipay wiring | WS / IPC + Web-Panel UI + smoke | 累计中 |\n| 7 | Shopping three-pack | Taobao / JD / Meituan + 共享 `shopping-base` Cookie 客户端 | 累计中 |\n| 7.5 | Mobile Extraction Layer | Android ADB + iOS iTunes backup | 累计中 |\n| 8 | EntityResolver | 规则 + embedding + LLM 三段 pipeline + ingest hook + UI 队列 + eval gate | 累计中 |\n| 9 | Travel four-pack | Amap / Baidu Map / Ctrip / 12306 + 共享 `travel-base` | 累计中 |\n| 10.1 | AIChatHistoryAdapter skeleton | 8 vendor stub + cookie-auth + schema-map + contract | +27 |\n| 10.2 | AIChatHistoryAdapter wiring | DeepSeek / Kimi / 通义 / 智谱 / 混元 / 千帆 / 扣子 / Dreamina 全 8 厂商接 h5 API + 共享 HttpClient | 累计 +52 |\n| 10.2 (+) | **AIChatHistoryAdapter 9th: 豆包 Doubao** v0.2 | cookie/snapshot 采集、cursor 分页与 `has_more` 已接通;私有 endpoint 字段仍待 Phase 10.4 真实 HAR fixture pin + 真账号 smoke | 结构/错误路径回归已覆盖;v1 sign-off 待账号 |\n| 11 | Analysis Skills | spending / relations / footprint / interests / timeline | 累计中 |\n| 12 v0.5 | WechatAdapter (frida-indep) | content-parser + key-extractor (MD5(IMEI+UIN)[:7]) + db-reader + normalize | +41 |\n| 13.1-13.4 | Social four-pack | Bilibili / Weibo / Douyin / Xiaohongshu (借 sjqz Python parser 移植) | 累计中 |\n| 13.5-13.7 | Messaging three-pack | QQ / Telegram / WhatsApp (借 sjqz parser + 加密 DB 解密) | 累计中 |\n| 13.8 (+) | **Social Toutiao (今日头条)** v0.2.1 dual-mode | snapshot + Android SQLite:profile / read_history / collection_article / search_history;桌面/CLI 与 Android ADB cookie/sign/root collector 已接线,真机端点命中率按 E2E runbook 验收 | snapshot/sqlite/ADB 回归已覆盖 |\n| 13.9 (+) | **Social Kuaishou (快手)** v0.2.1 dual-mode | snapshot + Android SQLite:profile / photo_history / user_collect / search_record;桌面/CLI 与 Android ADB cookie/sign/root collector 已接线,`__NS_sig3` 真机命中率按 E2E runbook 验收 | snapshot/sqlite/ADB 回归已覆盖 |\n| 12.6 §18.1-6 | KeyProvider DI + Frida agent + env-probe + Setup runbook | wechat/key-providers/{base,md5,frida} + frida-agent/{loader,wechat-key-hook} + env-probe;FridaKeyProvider lazy-load 容忍 frida binding 缺 | +43 scaffold |\n| 10.3.1 (+) | **AIChat WebView 鉴权向导 — cookie-capture-spec** | `cookie-capture-spec.js`:9 vendor matrix (loginUrl/cookieDomains/requiredCookies/postLoginPathHints/maxAgeHintDays/notes) + `classifyProbedCookies` 三态输入容忍 (object/Cookie[]/raw 串) + 自检 `validateCookieCaptureSpec` 启动期断言;设计稿 `Personal_Data_Hub_Phase_10_3_AIChat_WebView_Wizard.md` 13 章 4 IPC + 10 forward-looking traps | +21 scaffold |\n| 10.3.2 (+) | **AIChat WebView 鉴权向导 — wizard-controller** | `packages/personal-data-hub/lib/adapters/ai-chat-history/wizard-controller.js`(hub package 内,桌面 + cli 共享):5 入口 (openVendorLogin / probeCookies / registerVendor / rotateLoginPartition / cleanupOrphanPartitions) + `_deps` 注入 (sessionFactory / accountsStore / clock / logger) + `fallbackMode:\"paste\"` 分支(cc ui 端) + 防御性 cookie 投影(spec 白名单外的 cookie 不出 wizard 边界) + 7 类典型错误码 (UNKNOWN_VENDOR / SESSION_INIT_FAILED / PASTE_REQUIRED / REQUIRED_COOKIES_MISSING / VALIDATE_COOKIE_FAILED / ADAPTER_THREW / ACCOUNTS_LIST_FAILED) | +31 单测 |\n| 10.3.3 (+) | **AIChat WebView 鉴权向导 — 桌面 IPC + factory** | `desktop-app-vue/src/main/personal-data-hub/aichat-wizard-factory.js`:JSON-file accountsStore (0600, 写链串行化, ENOENT 容忍, 损坏 JSON 静默 fallback) + DEFAULT_VENDOR_SPECS → registerVendor 桥 (validateCookie 调度, HttpClient + CookieAuthSession 包装) + hubDir 维度 wizard singleton;`personal-data-hub-ipc.js` 注册 4 通道 (aichat-open-login / aichat-probe-cookies / aichat-register-vendor / aichat-rotate-login) | +20 单测 |\n| 10.3.4 (+) | **AIChat WebView 鉴权向导 — cc ui WS mirror** | `packages/cli/src/lib/personal-data-hub-aichat-wizard.js`(ESM):cli 端镜像 wizard wiring,`fallbackMode:\"paste\"` 硬编码(无 BrowserView) + 同一 JSON 文件 schema;`gateways/ws/personal-data-hub-protocol.js` 注册 4 个 `personal-data-hub.aichat-*` WS 主题 | +17 单测 |\n| 10.3.5 (+) | **AIChat WebView 鉴权向导 — HealthChecker** | `packages/personal-data-hub/lib/adapters/ai-chat-history/health-checker.js`:每 6h 自动巡检 (首次延迟 30s) + 4 类结果分类 (ok / failed / SPEC_VERSION_MISMATCH / ADAPTER_THREW) + 写回 entry.lastHealth + 同名 runOnce 防重入 + 注入式 timer seam (setInterval/setTimeout/clock) | +13 单测 |\n| 10.3.6 CLI (+) | **AIChat WebView 鉴权向导 — cc hub aichat 子命令** | `packages/cli/src/commands/hub.js`:`cc hub aichat list / login / probe / register / health / unregister` 6 个 verb 镜像 4 WS 主题 + HealthChecker;`--cookies \"<paste>\"` 走 paste-fallback;`--json` 全 verb 可机读;脚本/Plan A 手机内嵌终端可用,无需 Vue UI | +15 单测 |\n| 10.3 E2E (+) | **AIChat WebView 鉴权向导 — 全链路集成测试** | `__tests__/integration/aichat-wizard-end-to-end.test.js`:4 场景串 cookie-capture-spec → wizard-controller → accountsStore (real fs) → vendor-bridge → health-checker 整链;A 走通 (probe → register → health=ok) / B 过期 (cookie 失效后 health=failed) / C spec version mismatch / D orphan partition cleanup;无 Electron / 无网络 | +4 单测 |\n| 12.6.7 (+) | **WeChat adapter bootstrap 编排层** | `packages/personal-data-hub/lib/adapters/wechat/bootstrap.js`:把 §18.7 的 6 sub-phase 零件(env-probe / KeyProvider / WechatAdapter ctor)拼成一次性可注册入口;决策矩阵 md5/frida/unsupported + override + 全注入式测试 seam (`_probe`/`_md5Provider`/`_fridaProvider`/`_WechatAdapter`);T26-T28 trap 登记进设计稿 §18.10;89 WeChat tests 全绿 (75 既有 + 14 bootstrap) | +14 单测 |\n| 12.6.8 (+) | **WeChat register-wechat IPC + WS + privileged whitelist** | 4 IPC 通道 (`wechat-env-probe` / `register-wechat` / `unregister-wechat` / `list-wechat-accounts`) + WS 镜像;hub 暴露 `probeWechatEnv` / `registerWechatAdapter` / `unregisterWechatAdapter` / `listWechatAccounts` 四 method(desktop + cli 双 wiring 对称);`wechat-accounts.json` 0600 + idempotent re-register + scrubbed list;register/unregister-wechat 加入 `approvalChannelsForMobile` 防 mobile peer 注入恶意 dbPath;whitelist 单测同步拓展 | +15 单测 |\n| 12.6.9 (+) | **WeChat cc hub wechat CLI** | `cc hub wechat env-probe / register --uin --db --wechat-data-path [--force-provider] / list / unregister <uin>` 4 个 verb 镜像 4 WS 主题;`--json` 全 verb 可机读;脚本/Plan A 手机内嵌终端可用,无需 Vue UI(同 `cc hub aichat` 模式) | +14 单测 |\n| 12.6.10 (+) | **WeChat Vue UI 鉴权向导 (web-panel)** | `WechatWizard.vue` 3-step drawer:Step 1 env-probe checklist(adb/root/frida/wechat 5 行)→ Step 2 uin + dbPath + wechatDataPath + forceProvider 表单 → Step 3 result + reasons 反馈;`usePersonalDataHub.js` 加 `probeWechatEnv` / `registerWechat` / `listWechatAccounts` / `unregisterWechat` 4 method 镜像 WS topics;`PersonalDataHub.vue` 顶部加「添加 WeChat」按钮 + `WechatOutlined` 图标 | +6 composable 单测 |\n| 阶段性快照(v5.0.3.85 时) | — | — | 67 文件 / 1223 测试(含 Phase 10.2 集成 + E2E 6 + 3 场景;doubao scaffold + toutiao/kuaishou scaffold + analysis-skills backfill + WeChat Phase 12.6 §18.1-10 全套:KeyProvider + Frida agent + env-probe + Setup + 12.6.7 bootstrap + 12.6.8 IPC/WS wiring + 12.6.9 CLI + 12.6.10 Vue UI + 2026-05-21 Phase 10.3.1-10.3.6 全 land) |\n| **当前目录(2026-07-28)** | — | 生成器从所有导出 collector 构造并核对目录,包含 `email-imap` 与 `ai-chat-history`;多源输入经来源别名、raw observation 与字段级质量决策事务化归并 | **92 个采集契约 / 18 类**;能力清单、当前 readiness、完整续扫与真机/真账号验收分层记录 |\n\n> 全部路线图的代码路径已实施。剩余发布验收依赖外部条件:9 厂商 AIChat 真账号 smoke;Doubao 私有接口真实 HAR fixture pin;Toutiao/Kuaishou 等动态签名命中率;WhatsApp 用户备份 + key 的真机 UX;WeChat 8.0+ rooted-device E2E。\n\n## IPC / WS 接口完整列表\n\n中台所有功能通过 21 个对称接口暴露,**Electron IPC** 与 **cc ui / web-shell WS** 同时可用。命名约定:IPC 用 `personal-data-hub:<action>` (colon),WS 用 `personal-data-hub.<action>` (dot)。\n\n### 核心查询 / 健康 / 销毁(11 通道)\n\n| 通道 | 入参 | 出参 |\n| --------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |\n| `ask` | `{ question, options?: { acceptNonLocal?, useRag? } }` | `AskResult { answer, citations[], llmName, isLocal }` 或 `{ error }` |\n| `stats` | () | `{ vault: { events, persons, places, items, topics }, adapters[], hubDir, llm }` |\n| `health` | () | `{ vault: { ok, schemaVersion }, llm: { ok, isLocal, name }, kgSink: { ok }, ragSink: { ok } }` |\n| `list-adapters` | () | `Array<{ name, version, capabilities, sensitivity }>` |\n| `sync-adapter` | `{ name, options? }` | `SyncReport { ingested, kgTriples, ragDocs, durationMs }` |\n| `sync-all` | `{ options? }` | `Array<SyncReport>` |\n| `register-mock` | `{ name?, count?, seed? }` | `{ name, version }` — 仅开发/烟测用 |\n| `unregister` | `{ name }` | `{ ok }` |\n| `query-events` | `{ subtype?, since?, until?, actor?, adapter?, limit? }` | `Array<Event>` |\n| `recent-audit` | `{ since?, action?, limit? }` | `Array<AuditRow>` |\n| `destroy` (v5.0.3.72) | `{ confirm: true, alsoWipeAccounts?, alsoWipeMasterKey? }` | `{ ok, removed: string[] }` — UI 必须二次确认 |\n\n### Phase 5.6 — Email 配置向导(5 通道)\n\n| 通道 | 入参 | 出参 |\n| --------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------- |\n| `test-email-auth` | `{ account: { provider, email, authCode, … } }` | `{ ok, reason?, error? }` — 不注册,只验证 |\n| `register-email` | `{ account, opts? }` | `{ name, version, capabilities, sensitivity }` |\n| `unregister-email` | `{ email }` | `{ ok, removed }` — vault 数据保留可查 |\n| `list-email-accounts` | () | `Array<{ email, provider, folders, registeredAt, pdfPasswordHints[] }>` |\n| `event-detail` | `{ eventId }` | `{ event, classification, extraction: { template, confidence, fields, warnings, pdfExtraction } }` |\n\n### Phase 5.7 — 流式同步(2 通道)\n\n| 通道 | 入参 | 出参 |\n| --------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |\n| `sync-adapter-stream` | IPC: `{ name, options?, progressChannel }` / WS: `{ name, options? }` + `sender(payload)` 推流 | 同步进度事件 (`connecting` / `fetching` / `normalizing` / `done` / `error`) + 最终 `SyncReport` |\n| `sync-all-stream` | 同上 | `Array<SyncReport>` |\n\n### Phase 6 — Alipay 账单导入(4 通道)\n\n| 通道 | 入参 | 出参 |\n| ---------------------- | --------------------------------------------- | ------------------------------------------------ |\n| `register-alipay` | `{ account: { email, zipPassword? }, opts? }` | `{ name, version, capabilities, sensitivity }` |\n| `unregister-alipay` | `{ email }` | `{ ok, removed }` |\n| `list-alipay-accounts` | () | `Array<{ email, hasZipPassword, registeredAt }>` |\n| `import-alipay-bill` | `{ zipPath?, csvPath?, zipPassword? }` | `SyncReport` |\n\n> 全部通道在 [`desktop-app-vue/src/main/ipc/personal-data-hub-ipc.js`](https://github.com/chainlesschain/chainlesschain) 与 [`packages/cli/src/gateways/ws/personal-data-hub-protocol.js`](https://github.com/chainlesschain/chainlesschain) 双登记;任何 SPA 代码切换桌面 / web-shell 行为一致。\n\n## 前端集成\n\n### Electron 渲染进程\n\n```js\n// 自然语言问答\nconst result = await window.electron.invoke(\"personal-data-hub:ask\", {\n question: \"上个月我在淘宝买了啥总共花了多少?\",\n options: { useRag: true },\n});\n// → { answer: \"...\", citations: [{ eventId, ... }], llmName, isLocal }\nif (result.error) {\n /* 显示 */\n}\n\n// 健康状态\nconst health = await window.electron.invoke(\"personal-data-hub:health\");\n// → { vault: { ok, schemaVersion }, llm: { ok, isLocal, name }, kgSink, ragSink }\n\n// 流式同步(监听进度通道)\nconst progressChannel = `pdh-sync-${crypto.randomUUID()}`;\nwindow.electron.on(progressChannel, (_e, evt) => updateProgress(evt));\nconst report = await window.electron.invoke(\n \"personal-data-hub:sync-adapter-stream\",\n {\n name: \"email-imap\",\n options: { since: Date.now() - 7 * 86400 * 1000 },\n progressChannel,\n },\n);\n```\n\n### cc ui / web-shell (WS)\n\n```js\nconst ws = useShellMode().wsClient;\n\n// Q&A\nconst { result, error } = await ws.executeJson({\n type: \"personal-data-hub.ask\",\n question: \"上周和妈妈的对话提到她身体怎么样?\",\n});\n\n// 流式同步(onMessage 侧监听 type 后缀 .event 的中间消息)\nws.onMessage((msg) => {\n if (msg.type === \"personal-data-hub.sync-adapter-stream.event\") {\n updateProgress(msg.event);\n }\n});\nawait ws.executeJson({\n type: \"personal-data-hub.sync-adapter-stream\",\n name: \"alipay-bill\",\n});\n```\n\n### Web-Panel UI\n\n`packages/web-panel/src/views/PersonalDataHub.vue` 已有完整 UI:\n\n- **4 个健康卡片**:Vault schema 版本 + LLM provider 与本地标记 + KG sink 状态 + RAG sink 状态\n- **Ask box**:自然语言提问输入 + Markdown 回答 + 引用 chip(点击跳事件详情)\n- **Adapter 列表**:每行有 sync / unregister 操作 + 上次同步时间 + 敏感度标签\n- **Audit drawer**:滚动审计日志 (since / action / limit 过滤)\n- **配置抽屉**(Email / Alipay 分两 tab):测试授权 → 注册 → 自动同步\n\n桌面端 (Electron) 通过 `useShellMode().isEmbedded === true` 自动走 IPC,cc ui 走 WS — 同一份 SPA 代码。\n\n## 跨端入口\n\n中台核心是**桌面进程**(Electron 主进程):vault.db / 主密钥 / LLM Manager / KG / RAG 都落桌面。其它 shell 通过 IPC / WS 把请求转给桌面进程。\n\n### 桌面 Electron(默认)\n\n打开桌面应用 → 顶部菜单 `个人数据中台` → 进入 `PersonalDataHub.vue`(V6 shell 一级页面)。所有 IPC 通道 (`personal-data-hub:*`) 直连主进程 wiring,**零网络跳转**。\n\n### Web-Shell (`cc ui`)\n\n在任意机器装好 CLI(`npm i -g chainlesschain`)后:\n\n```bash\ncc ui # 起本地 web-shell(默认 http://localhost:7331)\n# 浏览器自动打开 → 左侧导航点「个人数据中台」\n```\n\n幕后:\n\n1. `cc ui` 启动 `packages/cli/src/lib/personal-data-hub-wiring.js` — 镜像桌面 wiring(同 `.chainlesschain/hub/` 目录、`OllamaClient` 作为默认 LLM)\n2. WS gateway 注册 21 个 `personal-data-hub.*` 主题 (`packages/cli/src/gateways/ws/personal-data-hub-protocol.js`)\n3. 浏览器加载 `packages/web-panel/dist/`(与桌面 V6 同一 SPA)→ `useShellMode().wsClient` 路由所有调用到 WS 网关\n\n**和桌面 Electron 等价性**:UI / IPC 形状 / 数据目录全相同;唯一差别是 LLM provider — `cc ui` 不复用桌面 LLMManager(无 IPC 通道),默认用本地 Ollama;如需切其它 provider 走 `cc llm provider <name>` 后重启。\n\n> 同一台机器同时跑桌面 + `cc ui` 会**共享 vault.db**(同 `.chainlesschain/hub/`),WAL 锁会自然串行化;推荐**二选一**避免并行 ingest 数据竞争。Phase 14+ 计划加 file-lock 协调。\n\n### CLI 直连 (`cc hub`)\n\n无 UI 场景(脚本 / SSH / Plan A 手机内嵌终端)下,**`cc hub <verb>`** 把同一组 IPC/WS 主题直接暴露为 11 个 verb,全部 `--json` 可机读:\n\n| Verb | 用途 | 关键 flag |\n| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |\n| `ask <question>` | 自然语言问答 | `--no-use-rag` / `--accept-non-local` / `--json` |\n| `stats` | vault 行数 + adapter list + 路径 | `--json` |\n| `health` | vault / llm / kgSink / ragSink 4 件套(彩色 [local]/[remote] 标签) | `--json` |\n| `list-adapters` | 已注册 adapter 列表 | `--json` |\n| `sync-adapter <name>` | 触发单 adapter ingest;支持单次临时 Cookie 采集,滴滴个人版另需显式官方 HTTPS 数据源 | `--input` / `--cookie-file` / `--account-id` / `--source-url` / `--since` / `--limit` |\n| `sync-all` | 顺序跑全部 adapter | 同上 |\n| `query-events` | 事件查询 | `--subtype` / `--since` / `--until` / `--actor` / `--adapter` / `--limit` |\n| `recent-audit` | 审计日志 | `--since` / `--action` / `--limit` |\n| `register-mock` | 注册 MockAdapter(dev/smoke 用) | `--name` / `--count` / `--seed` |\n| `run-skill <name>` | 5 内置分析 skill(spending/relations/footprint/interests/timeline) | `--since` / `--until` / `--json` |\n| `aichat list / login <v> / probe <v> --cookies / register <v> --cookies / health / unregister <v>` (v5.0.3.73) | AIChat WebView 鉴权向导 — 列 9 vendor / 拿 loginUrl / 校验 cookie / 注册 / 一次性 health / 注销 | `--cookies \"<paste>\"` / `--json` |\n| `wechat env-probe / register --uin --db --wechat-data-path / list / unregister <uin>` (v5.0.3.75) | WeChat adapter — adb 设备 + frida-server + WeChat 版本探测 / 按 env-probe 自动选 md5\\|frida provider 注册 / 列已注册 / 注销;`--force-provider md5\\|frida` 旁路 env-probe 建议 | `--json` 全 verb 可机读 |\n| `destroy --confirm` | 销毁 vault.db + WAL(不动 adapter 配置) | `--confirm`(必填,缺即拒) |\n\n示例:\n\n```bash\n# 健康检查(人类可读,含色标)\ncc hub health\n# ✓ vault schema=2\n# ✓ llm ollama:qwen2.5:7b-instruct [local]\n# ✓ kgSink\n# ✓ ragSink\n\n# 跑分析 skill 拿 JSON 给后续脚本用\ncc hub run-skill analysis.spending --since 1746028800000 --json | jq '.summary.totalSpend'\n\n# 临时 Cookie 直采(同理可用 travel-tongcheng / travel-didi)\n# account-id 只参与本地哈希水位隔离;Cookie 与明文账号标识不会写入 Vault\ncc hub sync-adapter travel-ctrip \\\n --cookie-file ./ctrip.cookie \\\n --account-id my-ctrip-account \\\n --json\n\n# 同一运行时 Cookie 契约也覆盖 game-genshin / edu-zuoyebang /\n# finance-alipay / edu-huawei-learning / netease-music / weread;\n# 默认 CLI 与 Electron 注册会把请求固定到各客户端的官方 API 主机。\n# 分页媒体源使用持久化请求配额;weread 每轮默认处理 25 本、最多 100 本,\n# 并以不含原始书籍 ID 的游标续扫,避免单轮超时或日配额中途失败。\n\n# 滴滴个人版没有内置订单端点:URL 仅本次使用,且只接受 xiaojukeji.com 官方 HTTPS 域\n# 若 URL 含会话签名参数,优先通过 CC_PDH_SOURCE_URL 注入,避免写入命令历史\ncc hub sync-adapter travel-didi-consumer \\\n --cookie-file ./didi.cookie \\\n --account-id my-didi-account \\\n --source-url 'https://api.xiaojukeji.com/captured/orders' \\\n --json\n\n# Plan A — Android 在 app 内嵌终端里直接跑(cc 已 bundle 进 APK)\n$ cc hub ask \"上个月跟妈妈消息多少?\"\n\n# 销毁(脚本场景必须显式 --confirm)\ncc hub destroy --confirm\n```\n\n**实现**:`packages/cli/src/commands/hub.js`(496 LOC)→ `getHub()` 复用 `personal-data-hub-wiring.js` 与 `cc ui` 同一份 LocalVault / OllamaClient / AdapterRegistry。Plan A 手机内嵌终端验证 (Xiaomi 24115RA8EC, 2026-05-20 T1/T2/T3 PASS)。\n\n### 移动端访问\n\n#### 当前状态(v5.0.3.72)\n\n| 端 | 入口 | 状态 |\n| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Android | `SeedRegistry` 元数据 (`namespace=personal-data-hub`, 21 method) + UI scaffold | 🚧 v0.1 — 元数据已注册(24 个远程 skill 之一),数据层 `PersonalDataHubCommands.kt` ✅、UI 9 文件 scaffold 落地于 `:app/remote/ui/personalDataHub/` ✅、`RemoteOperateScreen` 第 14 tab 接通待 Phase 14.1 收口 |\n| iOS | `RemoteOperateView` 第 16 tab \"数据中台\" + `Features/RemoteOperate/Views/PersonalDataHubViews.swift` (650 LOC, 3-tab) + `Modules/CoreP2P/Sources/RemoteSkills/PersonalDataHub/` (4 文件:Commands / Models / ViewModels / SyncEventDispatcher) + RemoteDependencies wired (含 fan-out 第 4 子流 hubSyncEventsStream buffer 256) | ✅ Phase 14.2 UI 完整落地 (`3db7b5a73` + `1d0c473a3`) — 22 method typed wrapper + 3 ViewModel (HubAsk / HubAdapters / HubAudit) 含隐私 gate sheet + citation chip + Phase 14.3.3.b eventId deep-link + race-guarded requestId;1491 LOC of tests (PersonalDataHubCommandsTests 628 + HubViewModelsTests 640 + HubSyncEventDispatcherTests 223)。docs-site 之前的 \"scaffold lost to race / needs redo\" 注释为 stale,2026-05-21 已修正 |\n\n#### 推荐路径(已有基础设施,落地工作小)\n\n移动端走**远程操控**链路(无需 Hub 在手机本地复刻 vault):\n\n```\niPhone / Android → P2P DC RPC (`hub.ask` 走 RemoteCommandClient)\n → 桌面 mobile-bridge 主进程\n → 桌面 personal-data-hub:ask IPC\n → AskResult 回流到手机\n```\n\n这套链路 framework 全 reuse 既有:\n\n- **Android**:iOS Phase 6 已有 `RemoteCommandClient.invoke()` 池 + `SeedRegistry` 23 skill。新加 `hub.ask` / `hub.stats` / `hub.health` / `hub.syncAdapter` 4 个 method 即可,工作量 ≈ 0.5 天 + UI(chat bubble 复用 Phase 5 AI Chat skill)≈ 1 天。\n- **iOS**:同理,复用 Phase 5 `RemoteAIChatViewModel` 模式,约 1.5 天。\n\n#### 数据流隐私约束\n\n无论手机端走哪条路径,**所有 LLM 推理仍在桌面进程执行**,敏感数据不离开桌面:\n\n1. 手机发送 NL question(带时间窗口 hint 即可,不附事实)\n2. 桌面 `AnalysisEngine.ask()` 跑全套(query-parse → vault facts → RAG → prompt-build → 隐私 gate → LLM)\n3. 仅 `AskResult { answer, citations, llmName, isLocal }` 回流手机\n4. 引用 chip 在手机端点击 → 走 `hub.eventDetail` RPC 拉单条事件\n\n> 隐私 Gate 在桌面侧执行:若桌面当前 LLM 非本地,手机端调用 `ask` 同样会被拒,UX 需把 `acceptNonLocal` 选项以\"开关\"暴露到手机端设置页。\n\n#### 路线图\n\n- ✅ Phase 14.0(Android 元数据):`SeedRegistry` 已加 `personal-data-hub` namespace + 21 method metadata(commit 在 v5.0.3.72 一同 land),含 risk 分级(ask/stats/query/audit = Safe;syncAdapter/register* = Mutating;unregister* = Privileged 强 ApprovalUI)。\n- 🚧 **Phase 14 完整设计稿已落**:[`docs/design/Personal_Data_Hub_Phase_14_Mobile_Native_Entry.md`](https://design.chainlesschain.com/Personal_Data_Hub_Phase_14_Mobile_Native_Entry.html) — 6 sub-phase 拆分 / 21 method UI 暴露矩阵 / 5 OQ 含推荐 / 12 forward-looking traps / 8 真机 E2E 场景。Phase 14.1 已启动(数据层 + 桌面白名单测试已落)。\n- 🚧 Phase 14.1(Android 接线 + UI):\n 1. ✅ 桌面 `mobile-skill-whitelist` 测试增强 — `personal-data-hub.*` namespace 通配验证 + 5 Privileged ApprovalUI 路由测试(23 单测,[`desktop-app-vue/src/main/remote/__tests__/mobile-skill-whitelist.test.js`](https://github.com/chainlesschain/chainlesschain))\n 2. ✅ 数据层 — `PersonalDataHubCommands.kt` 22 method typed wrapper(21 数据 + `runSkill` 分析)+ 27 模型 + 22 单测([`android-app/app/src/main/java/com/chainlesschain/android/remote/commands/PersonalDataHubCommands.kt`](https://github.com/chainlesschain/chainlesschain))。预发版静态审计发现所有多词 method 用了 camelCase(`listAdapters` / `syncAdapter` …),但桌面 `personal-data-hub-protocol.js` 注册的是 kebab-case(`list-adapters` / `sync-adapter` …)—— 17/22 method 跨端不匹配将在真机首调时 404。一次性 sed 修齐(生产 + 单测 + 桌面 whitelist 测试三处);iOS Phase 14.2 自始即用 kebab-case,无需调整。\n 3. ✅ **UI scaffold 已落** — 落地于 `:app/remote/ui/personalDataHub/`(与其它 remote skill UI 同 module,**未走原计划独立 feature module**,因 Hilt graph + RemoteCommandClient 注入路径都根植 `:app`):\n - `HubAskScreen` + `HubAskViewModel`(核心 NL Q&A,含隐私 gate 弹窗 + citation chip + bottom sheet 详情拉取)✅\n - `HubAdaptersScreen` + `HubAdaptersViewModel`(adapter 列表 + 同步触发)✅\n - `HubAuditScreen` + `HubAuditViewModel`(审计日志 + action filter chip)✅\n - `HubHealthCard`(vault / llm / kgSink / ragSink 四件套)✅\n - `AcceptNonLocalDialog`(非本地 LLM 二次确认)✅\n - `PersonalDataHubScreen`(3 tab container — 提问 / Adapter / 审计)✅\n - 6 个 `HubAskViewModel` 单测覆盖隐私 gate 三态路径(本地直通 / 非本地 dialog / 确认重发 / dismiss 不污染 / 普通错误 toast / health init)✅\n 4. ✅ **NavGraph 接通** — `RemoteOperateScreen` 加「个人数据中台」按钮 → 跳新路由 `Screen.PersonalDataHub`(`personal_data_hub`)→ 渲染 `PersonalDataHubScreen` 3-tab 容器。同时修了 9 个 UI scaffold + 1 个测试文件遗留的 `</content></invoke>` XML wrapper bug 与缺失的 class 闭合 brace(早前 Write tool 序列化吃掉了文件尾部)。\n 5. ✅ Phase 5 AI Chat ChatBubble 模式已 land — `HubAskScreen` 替换 flat `Surface` 为 `HubChatBubble`(左下 4dp / 右下 4dp 不对称圆角 = bubble tail;assistant `secondaryContainer` 左对齐、user `primaryContainer` 右对齐;max width 320dp 防止长答案撑爆屏宽)+ `HubBlinkingCursor`(500ms reverse-fade,\"▎\")填充 in-flight 状态。`HubAskUiState` 加 `submittedQuestion` field 把 \"用户输入框实时内容\" 与 \"已提交问题快照\" 解耦,允许用户在答案 bubble 显示时继续编辑下一题。streaming token-by-token wire-up 仍 defer — 当前 ViewModel 单射 ask;待 hub.ask 支持流式后改为 chunked emit + `HubBlinkingCursor` 续接尾部。\n- ✅ **Phase 14.2 完整 (iOS — 数据层 + UI scaffold + ViewModels 全 land)**:(1) **数据层** (`Modules/CoreP2P/Sources/RemoteSkills/PersonalDataHub/PersonalDataHubCommands.swift` actor + **22 method wrapper** 完整对齐 Android Phase 14.1 + `PersonalDataHubModels.swift` **16 Codable struct** + 双 path JSON shape fallback + `HubClassification` / `HubExtraction` eventDetail join;每个 method 单测都断言 wire `method` 名是 kebab-case,回归保护 Android Phase 14.1 真踩过的 camelCase / kebab-case mismatch([hidden-risk-traps.md #17](https://github.com/chainlesschain/chainlesschain/blob/main/docs/internal/hidden-risk-traps.md));(2) **ViewModels** (`HubViewModels.swift`) — `HubAskViewModel` 隐私 gate sheet + acceptNonLocal 重发 + citation chip eventDetail / `HubAdaptersViewModel` list+sync+syncStream + dispatcher 三 sink mirror progress/completedReports/errors / `HubAuditViewModel` filter chip + Phase 14.3.3.b eventId deep-link with race-guarded requestId;(3) **UI scaffold** (`Features/RemoteOperate/Views/PersonalDataHubViews.swift`, 650 LOC) — 3-tab segmented (提问 / Adapter / 审计) Inner View + StateObject 模式 (per [`ios_inner_view_stateobject_pattern`](https://github.com/chainlesschain/chainlesschain) memory);(4) **RemoteDependencies** wired (`self.hub = PersonalDataHubCommands(client:)` + `self.hubSyncDispatcher` + fan-out 第 4 子流 `hubSyncEventsStream` buffer 256);(5) **RemoteOperateView** 第 16 tab `.personalDataHub` 接入 `PersonalDataHubView(pcPeerId:)`。**1491 LOC tests** (PersonalDataHubCommandsTests 628 + HubViewModelsTests 640 + HubSyncEventDispatcherTests 223),CI 编绿 (`3db7b5a73` UI + `1d0c473a3` Phase 14.3.3.b deep-link)。先前 \"scaffold lost to untracked-file race / 待重做\" 注释为 stale (2026-05-21 audit 修正)。\n- ✅ Phase 14.3(双端):审计回查 + 同步进度推送 — 桌面 `personal-data-hub/route-mobile.js` 已注 `sendEventToPeer` 把 `sync-adapter-stream`/`sync-all-stream` 的 `personal-data-hub.sync.progress` 事件转发回手机;Android `HubSyncEventDispatcher.kt` + iOS `HubSyncEventDispatcher.swift` 第 4 子流(buffer 256)已 wire 进 RemoteDependencies;Phase 14.3.3.b eventId 审计 deep-link 双端 UI 全通(Android `HubAuditScreen.kt` clickable + ModalBottomSheet / iOS `HubAuditEventDetailSheet` 3-state sheet)\n- ⏳ Phase 14.4(真机 E2E):Mac + iPhone + Xiaomi 24115RA8EC + 真桌面,8 场景矩阵(详见设计文档 §8.3)\n\n## A8 — Android 完全独立路径(不依赖桌面)\n\n> **状态**:采集卡片和执行链已覆盖 Bilibili / 微博 / 抖音 / 小红书 / 今日头条 / 快手,并支持 cookie/snapshot 与可用时的 root SQLite 路径;当前环境没有 ADB 设备,动态签名和账号数据仍须按 E2E runbook 在真机验收。\n\nA8 与 Phase 14 远程操控是**两条独立路径**,回答不同的用户问题:\n\n| 路径 | UI 入口 | 数据落地 | 桌面在线要求 |\n| ---------------------- | ------------------------------------ | ------------------------------- | ------------ |\n| Phase 14(远程操控) | 个人数据中台 → 提问 / Adapter / 审计 | 桌面 vault | ✅ 需要在线 |\n| **A8(Android 独立)** | 个人数据中台 → **本机数据** | **APK 内** SQLCipher LocalVault | ❌ 完全离线 |\n\n### 工作原理\n\nA8 路径**不**经过桌面 hub — Android 端独立完成全部步骤:\n\n```\nWebView 内用户登录 → CookieManager.getCookie() 提取\n ↓\n 本机加密存储 (EncryptedSharedPreferences AES-256-GCM)\n ↓\n OkHttp × 4 端点 (history / favourite / dynamic / follow)\n ↓\n 拼装 snapshot.json 写 filesDir/.chainlesschain/staging/\n ↓\n LocalCcRunner spawn in-APK cc 子进程 (mksh + Termux Node)\n ↓\n cc hub sync-adapter --input <path> --json\n ↓\n adapter._syncViaSnapshot → registry → 本机 SQLCipher LocalVault\n ↓\n UI 显示 \"已同步 N 条\" + 上次同步时间\n```\n\n### 5 张卡片(v0.1)\n\n进 **本机数据** tab 看到:\n\n1. **本机数据** (system-data-android) ✅ — ContentResolver 通讯录 + PackageManager 已装应用\n2. **Bilibili** (social-bilibili) ✅ — 观看历史 / 收藏 / 动态 / 关注\n3. **微博** (social-weibo) ✅ — cookie API + snapshot/root SQLite 路径\n4. **抖音** (social-douyin) ✅ — cookie/SignBridge + snapshot/root SQLite 路径;线上签名命中率待真机验收\n5. **小红书** (social-xiaohongshu) ✅ — cookie/SignBridge + snapshot/root SQLite 路径;`X-S`/`b1` 版本兼容待真机验收\n6. **今日头条** (social-toutiao) ✅ — cookie/SignBridge + snapshot/root SQLite 路径;`_signature` 待真机验收\n7. **快手** (social-kuaishou) ✅ — cookie/SignBridge + snapshot/root SQLite 路径;`__NS_sig3` 待真机验收\n\n### Bilibili 同步细节\n\n| 数据 | API endpoint | 入 vault 类型 |\n| -------- | ------------------------------------------------------------------------ | ------------------------------------- |\n| 观看历史 | `/x/v2/history/cursor` | event (subtype=browse) + item (video) |\n| 收藏 | `/x/v3/fav/folder/created/list-all` + 每文件夹 `/x/v3/fav/resource/list` | event (subtype=like) + item (video) |\n| 动态 | `/x/polymer/web-dynamic/v1/feed/all` | event (subtype=browse) |\n| 关注 | `/x/relation/followings` | person (subtype=contact) |\n\n凭据存储:`pdh_social_bilibili` EncryptedSharedPreferences (AES-256-GCM + AndroidKeyStore master),键名 `cookie / uid / displayName / lastSyncAtMs / lastSyncCount`。退出登录走 `prefs.edit().clear().apply()`。\n\n### Plan A v0.1 引擎(A8 复用)\n\n- in-APK cc 子进程:`LocalCcRunner` → mksh symlink execve (W^X + SELinux 兼容)\n- 本机 vault:`@chainlesschain/personal-data-hub` LocalVault SQLCipher + bs3mc native binding\n- snapshot mode:JS adapter `_syncViaSnapshot(opts.inputPath)`(A8 v0.1 新增)+ 保留 `_syncViaSqlite` 旧模式\n- cli wiring 注册:`packages/cli/src/lib/personal-data-hub-wiring.js` 自动注册 `BilibiliAdapter` 在 boot 阶段\n\n### 线上验收注意项\n\n- 微博:WebView `SUB` cookie、UID 恢复和 `/api/container/getIndex` 已接线;真账号确认风控返回码。\n- 抖音:SignBridge / 本地 DB 双路已接线;确认当前 App/Web 版本的 msToken、X-Bogus 命中率。\n- 小红书:SignBridge / 本地 DB 双路已接线;确认当前 `X-S`、`b1` 组合。\n- 今日头条 / 快手:分别确认 `_signature` / `__NS_sig3`,失败时可退回用户本人 rooted-device SQLite 路径。\n- Bilibili:如官方收紧 wbi-\\* endpoint,按 runbook 更新 WBI fixture。\n\n详见设计文档:[`Adapter_Social_Cookie.md`](https://design.chainlesschain.com/Adapter_Social_Cookie.html)。\n\n## 真机采集与本地解密(Android root · 四方法框架)\n\n> **授权边界**:以下方法**仅用于用户本人拥有的、已 root 的设备 + 本人账号 + 本人 App**,目的是与\n> 用户自己的 PDH 互通。不要对他人账号 / 设备使用。明文副本含真实隐私,**只在本地仓库外目录留存、查完即删,绝不入 git**。\n\n桌面 / cookie 路径采不到的**端上加密数据**(IM 聊天、系统通讯录 / 短信 / 通话)需要在 root 设备上本地采集。\n按\"产出可靠性\"排序,四种**互补**方法:\n\n| 方法 | 适用 | 原理 | 产出 | 状态 |\n| -------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | --------------------------------------------------- |\n| **D 系统数据** | **任何 root 机第一步** | 通讯录 `contacts2.db` root 直读 + 短信 / 通话 `content query`(shell 权限)+ 媒体 `find` 元数据 | 单机实测 2 万+ events / 1 千+ 联系人 / 2 千+ 媒体 | ✅ 最高产可靠 |\n| **B 免密钥内存扫描** | 标准 SQLCipher App(微信 / QQ)已登录热进程 | App 打开加密库后,解密页驻留进程页缓存;root 读 `/proc/<pid>/mem`(不 ptrace、绕反调试)扫 `\"SQLite format 3\"` 页 → 重组 + 叶子页打捞 | 视 App / 版本 | ✅ 标准 SQLCipher 可(⚠️抖音 WCDB2 私有页格式不行) |\n| **A frida 抓 key** | 标准 SQLCipher 微信(`libWCDB.so`)已登录 | frida hook `sqlite3_key` 拿 raw key → 离线解密 | key | ✅ 但**登出态无 key**;解密优先走 C |\n| **C frida 在线解密** | WCDB/WCDB2 IM(头条 / 微信等,**无强反 frida**) | 借 App **自己已用正确 key 打开的连接** `ATTACH '' KEY ''` + `sqlcipher_export` 导明文副本,**绕开 cipher 参数**(拿 key 后离线复现 WCDB 自研 cipher 实测 72 组合全失败) | 明文库副本 | 🚧 **链路可达·端到端待验**(见下方诚实状态) |\n\n### 方法 D — 系统数据(先做这个,最可靠)\n\n```sh\nnode scripts/android/pdh-device-collect.mjs --serial <serial> # 通讯录+短信+通话+应用+媒体\ncc hub stats # 看 events/persons/items 增长\ncc hub search --adapter system-data-android # 抽查\n```\n\n全程明文、无解密、幂等(按 originalId 去重,可反复跑)。落库走 `system-data-android` adapter bridge 模式。\n\n### 方法 B — 免密钥内存扫描(标准 SQLCipher App)\n\n```sh\nadb -s <serial> push scripts/android/pdh-mem-sqlite-scan.sh /data/local/tmp/cc-scan.sh\nPID=$(adb -s <serial> shell su -c 'pidof <pkg>' | tr -d '\\r' | awk '{print $1}')\nadb -s <serial> shell su -c \"sh /data/local/tmp/cc-scan.sh $PID\" # dump → /data/local/tmp/ccmem/*.db\n# 连续 dump 对散页库 malformed → 叶子页打捞:\nfor f in dumps/*.db; do node scripts/android/pdh-sqlite-leaf-salvage.js \"$f\"; done\n```\n\n**前提**:App 须**前台热进程**(已登录 + 已打开过目标库,明文页才在内存);**别 force-stop 再启动**(冷启动触发反调试且未载库)。\n**Android 内一键**:HubLocal「一键 root 采集」按钮(`MemSalvageCollector`)封装了 su 扫描 → cp+chown → `cc hub salvage` 入库。\n\n### 方法 C — frida `sqlcipher_export` 在线解密\n\n```sh\n# 设备已 root;目标 App 已登录并前台进到「私信/消息」让库被查询。\nbash scripts/android/pdh-frida-decrypt.sh <serial> com.ss.android.article.news ~/pdh-data 60\n# → 明文副本 *.plain.db 拉到仓库外 ~/pdh-data(勿入 git),脚本自动清设备侧。\n```\n\n> ⚠️ **诚实状态(2026-06-18)**:目前是「`sqlcipher_export` **可达**」,**尚未真机产出明文库**。\n> 头条 `libwcdb2.so` 有导出符号、hook 命中 IM 库、export 被调用到,但 **rc=1 \"disk I/O error\"**\n> (疑 WCDB 自定义 VFS 管 `databases/` 目录 → 已把输出改写 `cache/`,未验证)。验证不了的原因:\n> **头条私信本机空**(无会话→无查询触发)+ **抖音有数据但反 frida**(`libmsaoaidsec` 压制 frida 输出 + 重负载把 USB 打 offline)。\n> 复现坑(写权限须 App 可写目录 / 必须等 libwcdb2 加载后再 attach / 点开会话才有查询 / DB*MATCH 用 `/encrypted*.\\*\\.db$/`/ export 走 onLeave 防重入)+ checklist 见`docs/internal/pdh-db-decryption-runbook.md` §3.6.1。\n\n### 各 App 解密状态\n\n| App | 引擎 | 可行方法 | 状态 |\n| --------------------------------------- | -------------------------------------- | --------------------------- | ----------------------------------- |\n| 系统数据(通讯录 / 短信 / 通话 / 媒体) | 明文 / content-query | **D** | ✅ 实测最可靠 |\n| 微信 `com.tencent.mm` | 标准 SQLCipher(`libWCDB.so`) | A / B / C | ✅ 机制验证(需登录;登出态无 key) |\n| QQ `com.tencent.mobileqq` | 普通 SQLite + XOR(IMEI) | root + IMEI(非 SQLCipher) | ✅ 算法移植 |\n| 今日头条 `com.ss.android.article.news` | WCDB2(`libwcdb2.so` **有符号**) | C(无反 frida) | 🚧 可达·待验(本机私信空) |\n| 抖音 `com.ss.android.ugc.aweme` | WCDB2 + `libEncryptor`/`libmsaoaidsec` | —(反 frida) | ❌ Method C 不实用;明文小库可拉 |\n\n### 数据库 Schema 字典(AI 正确解读 + adapter 适配的依据)\n\n各 App 解密出的本地库结构(表 / 字段 / 含义 / → PDH 实体 / 解读要点)已逐一登记,**完整导出 SQL(仅结构无数据,git tracked)**:\n\n| 文件(`docs/internal/reference/`) | 内容 |\n| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |\n| `wechat_schema.sql` | 微信 258 表完整 schema |\n| `toutiao_im_schema.sql` / `douyin_im_schema.sql` | 头条 / 抖音 ByteDance IM(12 表,`com.ss.android.im` 框架,两者通用;抖音另有 Room 版 `im_database_`) |\n| `toutiao_plaintext_db_schemas.sql` / `douyin_plaintext_db_schemas.sql` | 头条 19 + 抖音 25 个明文 app 库结构 |\n\n字段含义字典 `docs/internal/pdh-app-db-schemas.md`;解密方法详解 `docs/internal/pdh-db-decryption-runbook.md`(方法 A/B/C/D);可复现 skill:`.claude/skills/pdh-android-collect/SKILL.md`。\n\n### 复现脚本清单\n\n| 脚本 | 方法 |\n| ------------------------------------------------------------------------ | ------------------------------ |\n| `scripts/android/pdh-device-collect.mjs` | D 系统数据一键采 |\n| `scripts/android/pdh-mem-sqlite-scan.sh` + `pdh-sqlite-leaf-salvage.js` | B 免密钥内存扫描 + 叶子页打捞 |\n| `scripts/android/pdh-frida-decrypt.sh` + `pdh-frida-sqlcipher-export.js` | C frida 在线解密 |\n| `cc hub salvage <dump> --app <key>` | 打捞 dump → 按来源归属入 vault |\n\n### 采集器构建技能(`pdh-android-collector`)\n\n上面的四方法框架讲「怎么把数据取出来」;当你要为一个**新的中国 App 从零搭一个采集器**、或排查某个 `cc hub <platform>-adb-sync` 为什么返回 0 行时,Cowork / `cc agent` 内置了一个开发者向技能 **`pdh-android-collector`**(`category: integration`,可 `/pdh-android-collector \"<App 或包名>\"` 调用),把整套「建采集器」的方法论固化成可执行 runbook。\n\n> **与 `pdh-android-collect` 的区别**:`pdh-android-collect`(`.claude/skills/pdh-android-collect/SKILL.md`,配合本页 [使用示例](#使用示例) 的 `cc hub *-adb-sync` / `pdh-device-collect.mjs`)是**运行采集**——把已有采集器跑起来、把真机数据灌进 vault;`pdh-android-collector` 是**构建/调试采集器**——为尚无适配的 App 写一个新的 reader + adapter 接线。前者面向使用者,后者面向开发者。\n\n它编码的核心原则是 **「本地明文 SQLite 优先于 web API」**:App 的 web 接口(cookie schema、签名盐、endpoint 路径、权限码)持续漂移,而设备上的明文库稳定且免签名——只有当数据确实不在本地时才退回 web fetcher(且签名端点需桌面 SignBridge)。技能把流程切成 6 个阶段:\n\n| 阶段 | 目标 | 关键动作 |\n| ---------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Phase 0** 映射设备 | 永远先做 | `adb devices -l` / `su -c id`(release APK 不可调试,需 root)/ `pm list packages -3` → 映射到 `packages/personal-data-hub/lib/adapters/` |\n| **Phase 1** 试跑既有采集器读原因码 | 分流问题 | `cc hub <platform>-adb-sync --json`:`*_COOKIES_INCOMPLETE`=未登录 WebView;`*_IM_DB_ENCRYPTED`=SQLCipher 需 frida;`error_code`/404=端点漂移 |\n| **Phase 2** 审本地库(ROI 最高) | 找明文库 + uid | `head -c 16` 看 magic(`53514c697465…`=明文);`base64` 流式拉库(MIUI 安全);Node 内置 `node:sqlite` 读;**uid 常藏在文件名/表名或 `account_db.login_info`** |\n| **Phase 3** 建采集器 | 镜像既有模式 | `pull<Db>ViaSu` + `read<Data>` + `create<X>Extension`;归一化进**既有** adapter 的 snapshot kinds;CLI + Desktop **两处** wiring 都要接线 |\n| **Phase 4** 数据不在本地才走 web API | 兜底 | 先从设备取真 cookie;ByteDance 用 `err_no`/`error_code` 而非 `code`,HTTP 200 也可能 `data:[]`;签名端点(X-Bogus/`_signature`/WBI)只能走桌面 SignBridge |\n| **Phase 5** 真机验证(Electron harness) | 端到端 | `bs3mc` 原生 sqlite 与 SignBridge 只在 Electron 下加载;写一次性 `_harness.cjs` 用 `electron` 跑通 pull→read→normalize,**验完删除 harness + 拉下的库,绝不提交** |\n\n真机来源实证(chopin `5lhyaqu8lbwstc6x` / 小米,2026-06-11):这套配方拿下了头条(web profile 报 `error_code 16` 时从 `account_db.login_info` 恢复 uid)与抖音(`video_record.db` 的 `record_<uid>` = 900 条明文观看记录,绕开 SQLCipher IM 库)。\n\n> **发版闸**:任何 `packages/personal-data-hub/lib/**` 改动都需 bump pdh version + `npm publish` + Android `USR_VERSION`(否则真机走 fast-path 用旧代码;traps #27/#28)。桌面 dev(`npm run dev`,软链工作区 pdh)则即时生效。\n\n**关键文件**:`desktop-app-vue/src/main/ai-engine/cowork/skills/builtin/pdh-android-collector/SKILL.md`(技能声明即完整 runbook)。\n\n## 配置参考\n\n### 数据目录\n\n所有 hub 状态落在用户数据目录下统一根 `.chainlesschain/hub/`:\n\n| 平台 | 完整路径 |\n| ------- | ------------------------------------------------------------------------------- |\n| Windows | `%APPDATA%\\chainlesschain-desktop-vue\\.chainlesschain\\hub\\` |\n| macOS | `~/Library/Application Support/chainlesschain-desktop-vue/.chainlesschain/hub/` |\n| Linux | `~/.config/chainlesschain-desktop-vue/.chainlesschain/hub/` |\n\n文件清单:\n\n| 文件 | 用途 | 权限 |\n| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------- |\n| `vault.db` | SQLCipher AES-256 主库(events / persons / places / items / topics / kg_triples / raw_events / sync_watermarks / audit_log) | 由 SQLCipher 加密保护 |\n| `vault.db-wal`, `vault.db-shm` | SQLite WAL 日志 | 同上 |\n| `keys/vault.key` | FileKeyProvider 主密钥文件(hex) | **0600**(owner-only) |\n| `email-accounts.json` | 已注册邮箱配置(含 authCode) | **0600** |\n| `alipay-accounts.json` | 已注册支付宝账号(含 zipPassword) | **0600** |\n\n> ⚠️ Phase 3.5b 的 FileKeyProvider 是**临时方案**。Phase 4 将引入 Windows DPAPI / macOS Keychain / Linux libsecret 桥,主密钥与配置文件双重托管。设计稿 `Personal_Data_Hub_Architecture.md §7.1`。\n\n### 环境变量\n\ncli / web-shell 端用 `OllamaClient` 作为默认 fallback LLM(桌面 Electron 端走 `LLMManager` singleton):\n\n| 变量 | 默认 | 说明 |\n| --------------------- | ------------------------ | --------------------------------- |\n| `CC_HUB_OLLAMA_URL` | `http://localhost:11434` | Ollama HTTP 端点 |\n| `CC_HUB_OLLAMA_MODEL` | `qwen2.5:7b-instruct` | 模型 tag(先 `ollama pull` 拉好) |\n\n桌面端 LLM provider 切换走既有 `cc llm` / 设置面板,hub 通过 `CcLLMAdapter` 直接读 `LLMManager.provider` 与 `.client.model`。\n\n### 隐私 Gate 选项\n\n调用 `ask` 时控制行为:\n\n| 选项 | 默认 | 说明 |\n| ------------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `options.acceptNonLocal` | `false` | 若 LLM 非本地(Anthropic / Volcengine / Gemini / DeepSeek 等),默认拒绝。**显式 `true` 才放行** — UI 应警告并请求确认 |\n| `options.useRag` | `true` | 是否做 BM25 召回扩展上下文。关闭可加速但牺牲跨时段召回 |\n| `options.timeWindow` | (从 question 解析) | 显式覆盖时间窗口 |\n| `options.topK` | `10` | RAG 召回条数 |\n\n### Adapter 注册参数\n\n**Email**(详见 [`Adapter_Email_IMAP`](https://design.chainlesschain.com/Adapter_Email_IMAP.html)):\n\n```js\n{\n account: {\n provider: \"qq\" | \"gmail\" | \"163\" | \"custom\",\n email: \"you@example.com\",\n authCode: \"...\", // 授权码(非密码 — IMAP 专用)\n folders: [\"INBOX\"], // 可选,默认 INBOX\n imapHost?: \"...\", // custom provider 必填\n imapPort?: 993,\n },\n opts: {\n pdfPasswordHints: { \"10位身份证后6位\": \"...\", \"手机号\": \"...\" }, // PDF 解密候选\n sinceUid?: \"0\", // 强制水位(调试用)\n }\n}\n```\n\n**Alipay**(详见 [`Adapter_Alipay_Bill`](https://design.chainlesschain.com/Adapter_Alipay_Bill.html)):\n\n```js\n{\n account: {\n email: \"you@example.com\", // 仅作唯一标识;不连支付宝\n zipPassword: \"...\", // 支付宝邮件附带的 ZIP 解压密码\n },\n opts: { /* reserved */ }\n}\n// 导入时:\nhub.importAlipayBill({ zipPath: \"C:/Users/.../alipay_bill_2026Q1.zip\" });\n// 或解压后单 CSV:\nhub.importAlipayBill({ csvPath: \"C:/Users/.../alipay_record.csv\" });\n```\n\n## 性能指标\n\n| 场景 | 指标 | 测试基线 |\n| ---------------------------------------------------------------------------------------------- | --------------------------------------------------- | ---------------- |\n| 1k events 全流水线 ingest(normalize → vault.putBatch → kgSink → ragSink → watermark → audit) | < 30s 目标 / **实测 ~600ms** | Phase 2 |\n| LocalVault 单事件 put | < 5ms | Phase 1 |\n| `vault.queryEvents` 含 subtype + 时间窗口 | < 50ms / 10k 库 | Phase 1 |\n| `vault.findBySource(adapter, originalId)` 去重 | < 1ms(索引命中) | Phase 1 |\n| `vault.rotateKey` 5MB 库 | < 2s(WAL-safe) | Phase 1 |\n| EmailAdapter sync 100 封新邮件(QQ IMAP) | ~20-40s(受网络) | Phase 5.7 真账号 |\n| AlipayBillAdapter import 1 季度账单(~500 行 CSV) | < 5s | Phase 6 smoke |\n| AnalysisEngine `ask` 单次(Ollama qwen2.5:7b, RAG topK=10) | ~3-8s(受 GPU/CPU) | Phase 3 |\n| Hub 冷启动 (`getHub()` 首次) | ~100-200ms(vault open + migrations + bridge wire) | 测量 |\n| 全部 IPC 通道注册 | < 5ms | 测量 |\n\n> Phase 4 计划 partition 索引 + 异步 KG/RAG sink,进一步把 1k events 推到 < 200ms。\n\n## 测试覆盖率\n\n- **发布验证基线**:PDH 0.4.57 为 **282 个测试文件 / 4,528 项测试**;包含 adapter 契约/解析、事务化多源归并、来源别名、raw observation、完整续扫/分页失败闭合、分析管线、root 取证与设备级 schema 字典\n- **覆盖矩阵**:\n - `__tests__/ids.test.js` — UUID v7 唯一性 / 时间序性\n - `__tests__/schemas.test.js` — 5 类实体所有 subtype + 边界字段\n - `__tests__/batch.test.js` — empty / merge / validate / partition\n - `__tests__/key-providers.test.js` — InMemory + File 双实现 + 旋转\n - `__tests__/vault.test.js` — open / migrations / putBatch transactional / rollback / queryEvents / watermarks / rotateKey WAL-safe / destroy\n - `__tests__/registry.test.js` — health-gating / mid-sync failure recovery / sink failure tolerance / 1k events perf gate\n - `__tests__/kg-derive.test.js` + `rag-derive.test.js` — 三元组 / RagDoc 生成\n - `__tests__/query-parser.test.js` — 时间窗口 + 意图提取\n - `__tests__/prompt-builder.test.js` — system 不漏事实 + 用户角色挂事实 + citation 解析\n - `__tests__/analysis.test.js` — 全 E2E ask 路径 + 隐私 gate 拒绝 + citation 校验\n - `__tests__/bridges-cc-*.test.js` — CcLLMAdapter / CcKgSink / CcRagSink DI 单测\n - `__tests__/entity-resolver*.test.js` — Phase 8 三段 pipeline + ingest hook + vault schema\n - `__tests__/analysis-skills.test.js` — Phase 11 5 个内置分析 skill (spending / relations / footprint / interests / timeline)\n - `__tests__/shopping-adapters.test.js` — Phase 7 Taobao + JD + Meituan\n - `__tests__/travel-adapters.test.js` — Phase 9 Amap + Baidu Map + Ctrip + 12306\n - `__tests__/adapters/email-imap/*` — IMAP session + parser + classifier + transactions + pdf-extractor + 6 templates\n - `__tests__/adapters/alipay-bill/*` — csv-parser + zip-decryptor + counterparty + adapter\n - `__tests__/adapters/ai-chat-history*.test.js` — Phase 10.1 contract / schema-map / 8 vendor-spec + Phase 10.2 HttpClient + 各厂商响应解析\n - `__tests__/social-adapters.test.js` + `longtail-adapters.test.js` — Phase 13 social/messaging adapter (Bilibili / Weibo / Douyin / Xiaohongshu / QQ / Telegram)\n - `__tests__/whatsapp-adapter.test.js` + `adapters/messaging-whatsapp.test.js` + `adapters/whatsapp-backup-decryptor.test.js` + `adapters/whatsapp-adb-extension.test.js` — Phase 13.7b WhatsApp ADB public-backup pull + crypt14/crypt15 authenticated streaming decrypt + modern/legacy sqlite parse\n - `__tests__/wechat-adapter.test.js` — Phase 12 v0.5 frida-indep slice\n - `__tests__/mobile-extractor.test.js` — Phase 7.5 ADB + iTunes backup\n - `__tests__/sidecar-supervisor.test.js` + `sidecar-contacts-cross-validate.test.js` — Phase 4.5 Python sidecar 生命周期\n- **需外部条件覆盖**:真账号 IMAP / 真 Alipay ZIP / 9 厂商 AIChat cookie / 社交平台动态签名 / WhatsApp 真机备份 + 用户 key / rooted 微信 DB 端到端 — 走 `Personal_Data_Hub_E2E_Runbook.md` 手工跑\n\n跑测:\n\n```bash\ncd packages/personal-data-hub\nnpm test # 运行当前包的完整测试集\nnpx vitest run __tests__/vault.test.js # 单文件\n```\n\n## 安全考虑\n\n1. **本地优先存储**:vault.db 由 SQLCipher AES-256 加密;主密钥不入仓、不入云、不打包进 release。\n2. **0600 文件权限**:`keys/vault.key`、`email-accounts.json`、`alipay-accounts.json` 全部 owner-only — 同机其它用户也读不到。\n3. **临时 FileKeyProvider 已知风险**:当前主密钥与 IMAP authCode / ZIP 密码同目录文件落盘,威胁模型一致(同机有 root 即全失守)。Phase 4 升级为 DPAPI / Keychain 解决。\n4. **隐私 Gate 默认拒非本地 LLM**:`AnalysisEngine` 通过 `llm.isLocal` 判定;Ollama / 自托管 vLLM = 本地;Anthropic / Volcengine / Gemini / DeepSeek = 非本地,必须显式 `acceptNonLocal: true` 才放行。\n5. **Prompt 隔离**:所有 vault 事实放在 user role 消息 + 显式 `untrusted-data:` 标记,**system prompt 不含**任何用户数据 — 防 prompt injection 直接污染 system 指令。\n6. **Citation 强校验**:LLM 必须用 vault 真实 eventId 作 cite,`validateCitations` 比对找不到的会标 `citation-invalid` — 防幻觉 + 用户可点回查证。\n7. **全过程审计**:每一次 ingest / ask / register / unregister / sync 落 `audit_log`,含 actor / adapter / eventId / action / timestamp / context。任何 LLM 给出的答案都能反查到底是哪些事件、哪个 adapter、什么时间入库。\n8. **Validator 永不抛**:脏数据进 invalid 队列而非 crash,单条坏记录不会让攻击者通过制造畸形数据触发拒绝服务。\n9. **WAL-safe 密钥旋转**:`vault.rotateKey(newHex)` 内部接管 WAL 模式切换,旧密钥保留为 `vault:<id>:prev` 24h 回滚窗口;定期旋转可降低密钥泄露后果。\n10. **可销毁 (vault.destroy)**:单调用清理 vault.db + WAL + 关联密钥 — 兑现\"数据走得了\"承诺。\n11. **跨壳一致权限模型**:Hub 通过 Skill 系统挂载,自然继承 Skill marketplace permission whitelist(per `v6_plugin_ipc_permission_model`)。\n12. **adapter 上限敏感度声明**:每个 Adapter 必须实现 `dataDisclosure.sensitivity ∈ {low, medium, high, critical}`,UI 据此着色 + 高敏感 adapter 操作弹二次确认。\n\n> ⚠️ **未做的**:(a) U-Key/SIMKey 加固主密钥 — 设计中走 Phase 4 / 7;(b) 多用户共享 vault — 中台只服务\"我\",不做家庭 / 团队共享;(c) 对外发布数据 API — 设计明确不做。\n\n## 故障排查\n\n### `ask` 返回 `error: \"Analysis engine unavailable — LLM manager not initialized\"`\n\n- **检查**:`personal-data-hub:health` 返回 `llm.ok === false`\n- **原因**:桌面端 LLM Manager 尚未初始化,或 cli 端 Ollama 没起\n- **修复**:\n - 桌面:打开 设置 → AI 模型,选并测一个 provider\n - cli:`ollama serve` 起来 + `ollama pull qwen2.5:7b-instruct`;如果改了端口 `export CC_HUB_OLLAMA_URL=http://localhost:你的端口`\n\n### `ask` 返回 `error: \"Non-local LLM blocked — pass options.acceptNonLocal=true to override\"`\n\n- **原因**:隐私 Gate 拒绝。当前 active provider 不在本地白名单(Ollama / vLLM)\n- **修复**(按推荐顺序):\n 1. 切回 Ollama(设置 → AI 模型 → Ollama)\n 2. 或前端给用户一个明确的\"我接受把数据发到 X\"提示,确认后传 `{ acceptNonLocal: true }`\n\n### EmailAdapter 注册抛 `IMAP authentication failed`\n\n- **原因**:用了**登录密码**而不是**授权码**\n- **修复**:QQ 邮箱 / 163 / Gmail 等都需到邮箱\"安全 → IMAP/SMTP → 生成授权码\",把那串 16 位的字符填进去\n- **诊断**:先用 `test-email-auth` 通道(不持久化)单独验证\n\n### Alipay 导入抛 `Wrong password or corrupted zip`\n\n- **原因**:(a) `zipPassword` 拼错;(b) 从 App 导出时勾错了;(c) 邮件转发污染了 ZIP\n- **修复**:\n 1. 重新登录支付宝 App → 我的 → 账单 → 导出账单 → **明文记下密码** → 邮件正文应直接附 ZIP\n 2. 解压一次测试,能解开再传给 `import-alipay-bill`\n 3. 单独的 CSV 也支持:解压后 `importAlipayBill({ csvPath: \"...\" })`\n\n### LocalVault 抛 `SQLITE_NOTADB: file is not a database`\n\n- **原因**:(a) `vault.key` 被覆盖或换了;(b) vault.db 文件部分损坏\n- **修复**:\n 1. **不要删 vault.db**,先备份整目录\n 2. 检查 `keys/vault.key` 是否还在且 64 hex 字符\n 3. 如 `vault:default:prev` 还存在,临时改 key name 试旧密钥\n 4. 最后手段:`vault.destroy()` + 重 ingest(数据可重新从 adapter 拉)\n\n### Hub 冷启动卡 / `getHub()` 长时间不返回\n\n- **原因**:通常是 LocalVault migrations 在跑首次大量 schema 演化,或 SQLCipher 在初始化加密块\n- **修复**:等。100-200ms 是正常上限;如果 >10s 看 `hub.log` 是否有 \"Vault opened at … (schema vN)\" — 没有就 vault 文件损坏,按上一条处理\n\n### 端口 11434 冲突 / cc ui 启不来\n\n- **诊断**:`curl http://localhost:11434/api/tags` 应回 Ollama 模型列表\n- **修复**:换端口 `OLLAMA_HOST=0.0.0.0:11435 ollama serve` + `CC_HUB_OLLAMA_URL=http://localhost:11435`\n\n### Web-Panel 看不到 hub 卡片\n\n- **原因**:(a) WS gateway 没注册 hub protocol;(b) 走的是旧 build 的 web-panel\n- **修复**:升级到 v5.0.3.39+;确认 `packages/cli/src/gateways/ws/message-dispatcher.js` 引入了 `PERSONAL_DATA_HUB_HANDLERS` + `PERSONAL_DATA_HUB_STREAMING_HANDLERS`\n\n## 关键文件\n\n### Hub 核心(`packages/personal-data-hub/lib/`)\n\n| 文件 | 职责 |\n| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |\n| `constants.js` | enum 值(entity types / subtypes / capturedBy / …) |\n| `ids.js` | UUID v7 时间序 ID(RFC 9562, 零依赖) |\n| `schemas.js` | 5 类实体校验器(`validatePerson` / `validateEvent` / …) |\n| `batch.js` | NormalizedBatch helpers(empty / merge / validate / partition) |\n| `migrations.js` | 8 张表 + 版本演化 |\n| `vault.js` | LocalVault — SQLCipher AES-256 / 事务 / 查询 / 水位 / 审计 / 密钥旋转 |\n| `key-providers.js` | InMemory + File + KeyProvider 契约 |\n| `adapter-spec.js` | `PersonalDataAdapter` 接口 + `assertAdapter` |\n| `kg-derive.js` | UnifiedSchema → KG 三元组 |\n| `rag-derive.js` | UnifiedSchema → RagDoc |\n| `registry.js` | AdapterRegistry 全流水线 |\n| `mock-adapter.js` | deterministic MockAdapter(测试 + smoke) |\n| `query-parser.js` | NL question → 时间窗口 + 意图 |\n| `prompt-builder.js` | 事实摘要 + system/user 提示构造 + citation 解析 / 校验 |\n| `llm-client.js` | MockLLMClient + OllamaClient |\n| `analysis.js` | AnalysisEngine — `ask` 编排 + 隐私 gate |\n| `bridges/cc-llm-adapter.js` | 桌面 LLMManager 注入 |\n| `bridges/cc-kg-sink.js` | cc knowledge-graph addEntity / addRelation 桥 |\n| `bridges/cc-rag-sink.js` | cc BM25 写入桥 |\n| `adapters/email-imap/*.js` | EmailAdapter(IMAP / parser / classifier / templates / pdf / transactions) |\n| `adapters/alipay-bill/*.js` | AlipayBillAdapter(csv-parser / zip-decryptor / counterparty) |\n| `adapters/system-data/*.js` | SystemDataAdapter(Python sidecar 桥 / 通讯录 / 通话 / 短信 / WiFi) |\n| `adapters/_python-sidecar-base.js` | Python forensics-bridge 子进程生命周期管理(spawn / heartbeat / restart) |\n| `adapters/shopping-base/*` | Phase 7 共享 Cookie 客户端 + rate-limit |\n| `adapters/shopping-taobao/` + `shopping-jd/` + `shopping-meituan/` | Phase 7 三家电商 adapter |\n| `adapters/travel-base/*` | Phase 9 共享 LBS Cookie 客户端 |\n| `adapters/travel-amap/` + `travel-baidu-map/` + `travel-ctrip/` + `travel-12306/` | Phase 9 四家旅行 / 地图 adapter |\n| `adapters/ai-chat-history/{http-client,cookie-auth,schema-map,vendors/*}.js` | Phase 10 共享 HttpClient(cookie + rate-limit + 退避)+ 8 厂商 vendor-spec |\n| `adapters/social-{bilibili,weibo,douyin,xiaohongshu}/index.js` | Phase 13 四 social adapter(借 sjqz parser 移植) |\n| `adapters/messaging-{qq,telegram,whatsapp}/index.js` | Phase 13 三 messaging adapter(含 WhatsApp crypt14 / crypt15 本地直解) |\n| `adapters/wechat/{content-parser,key-extractor,db-reader,normalize,wechat-adapter}.js` | Phase 12 v0.5 WeChat frida-indep slice |\n\n### 桌面端接线(`desktop-app-vue/src/main/`)\n\n- `personal-data-hub/wiring.js` — 懒单例 + LLMManager 注入 + 邮箱/支付宝账号持久化\n- `ipc/personal-data-hub-ipc.js` — 全部 hub IPC 通道注册(v5.0.3.72 含 destroy / EntityResolver / 分析 skill / 流式同步)\n\n### CLI / Web-Shell 接线(`packages/cli/`)\n\n- `src/lib/personal-data-hub-wiring.js` — 镜像桌面 wiring(OllamaClient + 同 vault 目录)\n- `src/gateways/ws/personal-data-hub-protocol.js` — 全部 WS 主题镜像 IPC + `.event` 流式 + destroy\n\n### Web-Panel UI(`packages/web-panel/src/`)\n\n- `views/PersonalDataHub.vue` — 桌面 + cc ui 共用页\n- `composables/usePersonalDataHub.js` — shell-agnostic 调用层\n\n### 测试(`packages/personal-data-hub/__tests__/`)\n\n26 个 `*.test.js` 文件,~400+ 单测\n\n### 设计文档(`docs/design/`)\n\n- `Personal_Data_Hub_Architecture.md` — 主架构 + 13-Phase 路线图\n- `Personal_Data_Hub_E2E_Runbook.md` — 真账号 E2E 手工验收脚本\n- `Personal_Data_Hub_EntityResolver.md` — 跨源人物/地点消歧设计\n- `Adapter_Email_IMAP.md` — 邮箱 adapter 细节\n- `Adapter_Alipay_Bill.md` — 支付宝 adapter 细节\n- `Adapter_AIChat_History.md` — 8 家国产 AI + ChatGPT/Claude 历史抓取设计\n- `Adapter_WeChat_SQLCipher.md` — 微信 SQLCipher 直读设计\n\n## 使用示例\n\n### 1. 注册邮箱并增量同步\n\n```js\n// 1) 测试授权(不持久化)\nconst probe = await window.electron.invoke(\n \"personal-data-hub:test-email-auth\",\n {\n account: {\n provider: \"qq\",\n email: \"me@qq.com\",\n authCode: \"xxxxxxxxxxxxxxxx\", // 邮箱设置里生成的 16 位授权码\n },\n },\n);\nif (!probe.ok) {\n console.error(\"auth failed:\", probe.reason || probe.error);\n return;\n}\n\n// 2) 正式注册(持久化到 email-accounts.json)\nawait window.electron.invoke(\"personal-data-hub:register-email\", {\n account: { provider: \"qq\", email: \"me@qq.com\", authCode: \"...\" },\n opts: { pdfPasswordHints: { 身份证后6位: \"123456\" } },\n});\n\n// 3) 流式同步带进度\nconst ch = `pdh-${Date.now()}`;\nwindow.electron.on(ch, (_e, evt) => {\n console.log(evt.kind, evt.adapter, evt.partition, evt.detail);\n});\nconst report = await window.electron.invoke(\n \"personal-data-hub:sync-adapter-stream\",\n {\n name: \"email-imap\",\n options: { since: Date.now() - 30 * 86400 * 1000 }, // 最近 30 天\n progressChannel: ch,\n },\n);\nconsole.log(\"ingested\", report.ingested, \"events\");\n```\n\n### 2. 导入支付宝账单(ZIP)\n\n```js\n// 用户在桌面 PersonalDataHub 页面拖入 ZIP 文件 → 自动调下面\n// 从支付宝邮件正文复制下来的解压密码:\nconst zipPassword = \"...\"; // ← 替换为真实密码\nawait window.electron.invoke(\"personal-data-hub:register-alipay\", {\n account: { email: \"me@anything.com\", zipPassword },\n});\nconst report = await window.electron.invoke(\n \"personal-data-hub:import-alipay-bill\",\n { zipPath: \"C:/Users/me/Downloads/alipay_bill_2026Q1.zip\" },\n);\nconsole.log(`ingested ${report.ingested} payment events`);\n```\n\n### 3. 自然语言问问题\n\n```js\nconst { answer, citations, llmName, isLocal } = await window.electron.invoke(\n \"personal-data-hub:ask\",\n {\n question: \"上个月我在淘宝买了啥总共花了多少?给个清单。\",\n options: { useRag: true },\n },\n);\nconsole.log(answer);\nconsole.log(\n \"依据:\",\n citations.map((c) => c.eventId),\n);\n// 点击 citation → event-detail 查原始事件\n```\n\ncli 同等命令(走 WS):\n\n```bash\ncc ui # 起本地 web-shell\n# 在 SPA 内或通过 WS 直接发:\n# { \"type\": \"personal-data-hub.ask\", \"id\": \"1\", \"question\": \"...\" }\n```\n\n### 4. 手动注册 MockAdapter 跑 smoke\n\n开发时验证流水线(vault + KG + RAG + audit 一条龙)通畅:\n\n```js\nawait window.electron.invoke(\"personal-data-hub:register-mock\", {\n name: \"mock\",\n count: 100,\n seed: 42,\n});\nconst stats = await window.electron.invoke(\"personal-data-hub:stats\");\nconsole.log(\"vault has\", stats.vault.events, \"events after mock sync\");\n```\n\n### 5. 审计回查\n\n```js\nconst audit = await window.electron.invoke(\"personal-data-hub:recent-audit\", {\n action: \"ingest\",\n since: Date.now() - 86400 * 1000,\n limit: 50,\n});\nfor (const row of audit) {\n console.log(row.action, row.adapter, row.eventId, new Date(row.at));\n}\n```\n\n### 6. 数据销毁(一键 wipe)\n\n> ✅ v5.0.3.72 起暴露专门 IPC + WS 主题 `destroy`(双端对称登记)。要求显式 `confirm: true` 防意外调用;可选 `alsoWipeAccounts` / `alsoWipeMasterKey` 进一步扩大清理范围。UI 调用前必须二次确认。\n\n```js\n// 桌面 Electron\nconst r = await window.electron.invoke(\"personal-data-hub:destroy\", {\n confirm: true, // 必须;缺省直接返回 error\n alsoWipeAccounts: true, // 可选:清 email-accounts.json + alipay-accounts.json\n alsoWipeMasterKey: false, // 可选:清 keys/vault.key(**会让残留 -wal/-shm 永远打不开,谨慎用**)\n});\n// → { ok: true, removed: [\"C:/.../vault.db\", \"C:/.../vault.db-wal\", ...] } 或 { error }\n\n// cc ui / web-shell\nawait ws.executeJson({\n type: \"personal-data-hub.destroy\",\n confirm: true,\n alsoWipeAccounts: true,\n});\n```\n\n幕后:调 `hub.vault.destroy()` 删 vault.db + WAL + SHM,按 flag 清账号配置 / 主密钥,最后 `close()` 释放单例 — 下一次 `getHub()` 会从空白重建。\n\n## 相关文档\n\n### 同类核心功能\n\n- [个人 AI 知识库](/chainlesschain/personal-ai-knowledge-base) — 采集之后:analysis.\\* 分析技能 + RAG 问答 + 决策辅助\n- [知识库管理](/chainlesschain/knowledge-base) — 中台派生的 KG / RAG 写入目标\n- [数据加密](/chainlesschain/encryption) — SQLCipher / FileKeyProvider / Phase 4 升级到平台 Keystore\n- [U盾集成](/chainlesschain/ukey) / [SIMKey集成](/chainlesschain/simkey) / [蓝牙U-Key](/chainlesschain/ble-ukey) — 硬件加固主密钥(Phase 4 + Phase 7 路线)\n- [AI模型配置](/chainlesschain/ai-models) — Ollama / 各 provider 切换;隐私 Gate 直接读这里\n- [永久记忆系统](/chainlesschain/permanent-memory) — 中台 vault 与既有记忆系统的关系\n\n### 多智能体 / 数据消费\n\n- [Cowork 多智能体协作](/chainlesschain/cowork) — 分析复杂跨源问题时由 Cowork 编排\n- [日常任务协作](/chainlesschain/web-cowork) — Web 端 cowork\n- [上下文工程](/chainlesschain/context-engineering) — Hub 数据如何注入 LLM 上下文\n\n### 同步 / 部署\n\n- [移动端同步](/chainlesschain/mobile-sync) — Hub 数据跨设备的 P2P 同步路径\n- [Git同步](/chainlesschain/git-sync) — 备份 vault 的外部选项\n\n### 设计文档(design.chainlesschain.com)\n\n- [个人数据中台 主架构](https://design.chainlesschain.com/Personal_Data_Hub_Architecture.html) — 13-Phase 路线图、OQ 决策、整体架构图\n- [Phase 11 Analysis Skills](https://design.chainlesschain.com/Personal_Data_Hub_Analysis_Skills.html) — 5 内置 skill (spending/relations/footprint/interests/timeline) 输入输出契约 + Trap + 新 skill checklist\n- [EntityResolver 设计](https://design.chainlesschain.com/Personal_Data_Hub_EntityResolver.html) — 跨源 Person / Place 消歧(A+B+C+D 四段式)\n- [E2E Runbook](https://design.chainlesschain.com/Personal_Data_Hub_E2E_Runbook.html) — 真账号端到端手工验收脚本\n- [Adapter — Email (IMAP)](https://design.chainlesschain.com/Adapter_Email_IMAP.html)\n- [Adapter — 支付宝账单](https://design.chainlesschain.com/Adapter_Alipay_Bill.html)\n- [Adapter — AI 对话历史 (8 家国产 + ChatGPT/Claude)](https://design.chainlesschain.com/Adapter_AIChat_History.html)\n- [Phase 10.3 AIChat WebView 鉴权向导](https://design.chainlesschain.com/Personal_Data_Hub_Phase_10_3_AIChat_WebView_Wizard.html) — 9 家国产 AI 统一 cookie 抓取向导:桌面 Electron BrowserView A 级 / cc ui 粘贴 fallback C 级 / 移动端经由 Phase 14 远程;4 新 IPC + 9 vendor cookie matrix + 10 forward-looking traps + 4 天工期估\n- [Adapter — 微信 SQLCipher](https://design.chainlesschain.com/Adapter_WeChat_SQLCipher.html)\n- [Phase 12.9 WeChat 真机 E2E Runbook (frida-dep path)](https://design.chainlesschain.com/Personal_Data_Hub_Phase_12_9_WeChat_RealDevice_E2E_Runbook.html) — 11 场景验收 (首次 ingest / 解密 spot-check / ask Q&A / citation 反查 / 大库性能 / 增量 / hook resilience / 反检测 / 失败恢复 / 隐私 gate / 24h 长稳);与 `Adapter_WeChat_SQLCipher_Frida_Setup.md`(setup-only)和 `Personal_Data_Hub_E2E_Runbook.md §11`(注册流程)三件套组成 WeChat 12.6 完整验收链\n- [Adapter — 系统数据 (Android 通讯录 / 通话 / 短信 / WiFi)](https://design.chainlesschain.com/Adapter_System_Data.html)\n- [Adapter — 电商 Cookie (Taobao / JD / Meituan)](https://design.chainlesschain.com/Adapter_Shopping_Cookie.html)\n- [Adapter — 出行 / LBS (Amap / Baidu / Ctrip / 12306)](https://design.chainlesschain.com/Adapter_Travel_LBS.html)\n- [Adapter — Social + Messaging (Bilibili / Weibo / Douyin / 小红书 / QQ / Telegram / WhatsApp)](https://design.chainlesschain.com/Adapter_Social_Messaging.html)\n- [Python Sidecar 架构 (forensics-bridge fork from sjqz)](https://design.chainlesschain.com/Personal_Data_Hub_Python_Sidecar.html)\n- [PDH vs sjqz 借鉴方案对比](https://design.chainlesschain.com/Personal_Data_Hub_sjqz_Comparison.html)\n- [Phase 14 移动端原生入口(Android + iOS)](https://design.chainlesschain.com/Personal_Data_Hub_Phase_14_Mobile_Native_Entry.html) — Plan B 远程操控模式:Mobile-bridge → typed RPC → SwiftUI/Compose UI;21 method 全 wire;6 sub-phase + 8 真机 E2E 场景\n- [Phase 14.3 双端流式同步进度 + 审计回查](https://design.chainlesschain.com/Personal_Data_Hub_Phase_14_3_Sync_Audit_Streaming.html) — 14.3 详细设计:wire schema (`personal-data-hub.sync.progress` 4+1 状态机) + Android/iOS HubSyncEventDispatcher 模板 + fan-out 第 4 子流 + 双端 UI 模式 + 24+ 新单测目标\n- [代码路径 → v1 Fixture Pin Protocol](https://design.chainlesschain.com/Personal_Data_Hub_Fixture_Pin_Protocol.html) — 通用方法论:HAR/DB capture → field-rename-map → fixture-driven test → 真账号 smoke → v1 sign-off gate;当前覆盖 Doubao (Phase 10.4) + Toutiao/Kuaishou (Phase 13.10) 等待验收路径\n- [Plan A — Android 本机独立 PDH via in-APK cc CLI](https://design.chainlesschain.com/Personal_Data_Hub_Android_Standalone_Cc.html) — 不依赖桌面:vault / Ollama 兜替(llama.rn 端侧)/ adapter pipeline 全部在手机本地跑;APK 已 bundled cc 验证 (Phase 2.5);跨 app sandbox 5 路径分级;~10-16d 工时\n- [Cc CLI 自然语言驱动 — 管理手机其他 App + 数据](https://design.chainlesschain.com/Cc_NL_Phone_App_Manager.html) — `cc nl \"<NL>\"` 顶层命令;4 类 intent (query/ingest/control/action) + 端侧 1.5-3B LLM + 规则 fast-path + 隐私 gate + 高敏 confirm;~5d 工时\n\n---\n\n> **反馈与跟进**:中台是 ChainlessChain 兑现\"数据回归个人\"承诺的支柱。所有 13-Phase 路线图 adapter 已落地(v5.0.3.72),下一步主攻方向:Phase 10.3 AIChat 8 厂商 WebView 真账号 smoke + Phase 12.6+ WeChat 8.0+ frida-dep 路径 + Phase 14 移动端原生入口。欢迎在 GitHub issues 中标注 `area:personal-data-hub` 提需求 / bug / 新 Adapter 提议。\n"}}}}