mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-06 01:54:11 +08:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5ba7c0f6c | ||
|
|
1cf0d92ec2 | ||
|
|
02930bd56b | ||
|
|
4061ae48c4 | ||
|
|
ecd5085e51 | ||
|
|
6bc8b7de95 | ||
|
|
e79e33773f | ||
|
|
0c0301d811 | ||
|
|
89f6ac6804 | ||
|
|
f14c3299bc | ||
|
|
a73828b4d6 | ||
|
|
6244bf0405 | ||
|
|
90852c7788 | ||
|
|
3b842ed290 | ||
|
|
673e1d117a | ||
|
|
f64f619713 | ||
|
|
a742fa0f8a | ||
|
|
6894c7e80b | ||
|
|
203100431b | ||
|
|
e8b9bcae92 | ||
|
|
052351ab5b | ||
|
|
9dd84e3416 | ||
|
|
211c25d969 | ||
|
|
275684d319 | ||
|
|
0f8a47e8f6 | ||
|
|
303c840464 | ||
|
|
b15008fbce | ||
|
|
a8cf3e1ad6 | ||
|
|
0515ef6e8b | ||
|
|
777d5df573 | ||
|
|
c5f379ba01 | ||
|
|
145d38c9bd | ||
|
|
eab957ce00 | ||
|
|
b5fb077ad6 | ||
|
|
ebcbb11cb2 | ||
|
|
a1413dd1b3 | ||
|
|
4e6ee2db25 | ||
|
|
8e744597d1 | ||
|
|
dfa8b541b4 | ||
|
|
1dc55f8811 | ||
|
|
501d9a05d4 | ||
|
|
229d51cd18 | ||
|
|
40e61b30d6 | ||
|
|
3c3ce55842 | ||
|
|
e3e61bcae9 | ||
|
|
dfca4d60ee | ||
|
|
e671b45948 | ||
|
|
b00113d212 | ||
|
|
9b926d1a1e | ||
|
|
98c9f1a830 | ||
|
|
46ac591fe8 | ||
|
|
bf66b095c7 | ||
|
|
5228581324 | ||
|
|
c9c704e671 | ||
|
|
16d4c7c646 | ||
|
|
39056292b7 | ||
|
|
87ffd283ce | ||
|
|
8eb42816f1 | ||
|
|
ebdf64c0b9 | ||
|
|
caab5f476e | ||
|
|
1998f3ae8a | ||
|
|
5ff2a43b70 | ||
|
|
3cd842ca1a |
@@ -2,9 +2,32 @@
|
||||
|
||||
- **CLI Tools Usage**: @~/.claude/workflows/cli-tools-usage.md
|
||||
- **Coding Philosophy**: @~/.claude/workflows/coding-philosophy.md
|
||||
- **Context Requirements**: @~/.claude/workflows/context-tools.md
|
||||
- **Context Requirements**: @~/.claude/workflows/context-tools-ace.md
|
||||
- **File Modification**: @~/.claude/workflows/file-modification.md
|
||||
- **CLI Endpoints Config**: @.claude/cli-tools.json
|
||||
|
||||
## Agent Execution
|
||||
## CLI Endpoints
|
||||
|
||||
- **Always use `run_in_background = false`** for Task tool agent calls to ensure synchronous execution and immediate result visibility
|
||||
**Strictly follow the @.claude/cli-tools.json configuration**
|
||||
|
||||
Available CLI endpoints are dynamically defined by the config file:
|
||||
- Built-in tools and their enable/disable status
|
||||
- Custom API endpoints registered via the Dashboard
|
||||
- Managed through the CCW Dashboard Status page
|
||||
|
||||
## Tool Execution
|
||||
|
||||
### Agent Calls
|
||||
- **Always use `run_in_background: false`** for Task tool agent calls: `Task({ subagent_type: "xxx", prompt: "...", run_in_background: false })` to ensure synchronous execution and immediate result visibility
|
||||
- **TaskOutput usage**: Only use `TaskOutput({ task_id: "xxx", block: false })` + sleep loop to poll completion status. NEVER read intermediate output during agent/CLI execution - wait for final result only
|
||||
|
||||
### CLI Tool Calls (ccw cli)
|
||||
- **Always use `run_in_background: true`** for Bash tool when calling ccw cli:
|
||||
```
|
||||
Bash({ command: "ccw cli -p '...' --tool gemini", run_in_background: true })
|
||||
```
|
||||
- **After CLI call**: If no other tasks, stop immediately - let CLI execute in background, do NOT poll with TaskOutput
|
||||
|
||||
## Code Diagnostics
|
||||
|
||||
- **Prefer `mcp__ide__getDiagnostics`** for code error checking over shell-based TypeScript compilation
|
||||
|
||||
47
.claude/cli-tools.json
Normal file
47
.claude/cli-tools.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"tools": {
|
||||
"gemini": {
|
||||
"enabled": true,
|
||||
"isBuiltin": true,
|
||||
"command": "gemini",
|
||||
"description": "Google AI for code analysis"
|
||||
},
|
||||
"qwen": {
|
||||
"enabled": true,
|
||||
"isBuiltin": true,
|
||||
"command": "qwen",
|
||||
"description": "Alibaba AI assistant"
|
||||
},
|
||||
"codex": {
|
||||
"enabled": true,
|
||||
"isBuiltin": true,
|
||||
"command": "codex",
|
||||
"description": "OpenAI code generation"
|
||||
},
|
||||
"claude": {
|
||||
"enabled": true,
|
||||
"isBuiltin": true,
|
||||
"command": "claude",
|
||||
"description": "Anthropic AI assistant"
|
||||
}
|
||||
},
|
||||
"customEndpoints": [],
|
||||
"defaultTool": "gemini",
|
||||
"settings": {
|
||||
"promptFormat": "plain",
|
||||
"smartContext": {
|
||||
"enabled": false,
|
||||
"maxFiles": 10
|
||||
},
|
||||
"nativeResume": true,
|
||||
"recursiveQuery": true,
|
||||
"cache": {
|
||||
"injectionMode": "auto",
|
||||
"defaultPrefix": "",
|
||||
"defaultSuffix": ""
|
||||
},
|
||||
"codeIndexMcp": "ace"
|
||||
},
|
||||
"$schema": "./cli-tools.schema.json"
|
||||
}
|
||||
@@ -5,7 +5,7 @@ argument-hint: "[--dry-run] [\"focus area\"]"
|
||||
allowed-tools: TodoWrite(*), Task(*), AskUserQuestion(*), Read(*), Glob(*), Bash(*), Write(*)
|
||||
---
|
||||
|
||||
# Clean Command (/clean)
|
||||
# Clean Command (/workflow:clean)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -20,9 +20,9 @@ Intelligent cleanup command that explores the codebase to identify the developme
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
/clean # Full intelligent cleanup (explore → analyze → confirm → execute)
|
||||
/clean --dry-run # Explore and analyze only, no execution
|
||||
/clean "auth module" # Focus cleanup on specific area
|
||||
/workflow:clean # Full intelligent cleanup (explore → analyze → confirm → execute)
|
||||
/workflow:clean --dry-run # Explore and analyze only, no execution
|
||||
/workflow:clean "auth module" # Focus cleanup on specific area
|
||||
```
|
||||
|
||||
## Execution Process
|
||||
@@ -321,7 +321,7 @@ if (flags.includes('--dry-run')) {
|
||||
**Dry-run mode**: No changes made.
|
||||
Manifest saved to: ${sessionFolder}/cleanup-manifest.json
|
||||
|
||||
To execute cleanup: /clean
|
||||
To execute cleanup: /workflow:clean
|
||||
`)
|
||||
return
|
||||
}
|
||||
1467
.claude/commands/workflow/docs/analyze.md
Normal file
1467
.claude/commands/workflow/docs/analyze.md
Normal file
File diff suppressed because it is too large
Load Diff
1265
.claude/commands/workflow/docs/copyright.md
Normal file
1265
.claude/commands/workflow/docs/copyright.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ Intelligent lightweight planning command with dynamic workflow adaptation based
|
||||
- Intelligent task analysis with automatic exploration detection
|
||||
- Dynamic code exploration (cli-explore-agent) when codebase understanding needed
|
||||
- Interactive clarification after exploration to gather missing information
|
||||
- Adaptive planning strategy (direct Claude vs cli-lite-planning-agent) based on complexity
|
||||
- Adaptive planning: Low complexity → Direct Claude; Medium/High → cli-lite-planning-agent
|
||||
- Two-step confirmation: plan display → multi-dimensional input collection
|
||||
- Execution dispatch with complete context handoff to lite-execute
|
||||
|
||||
@@ -38,7 +38,7 @@ Phase 1: Task Analysis & Exploration
|
||||
├─ Parse input (description or .md file)
|
||||
├─ intelligent complexity assessment (Low/Medium/High)
|
||||
├─ Exploration decision (auto-detect or --explore flag)
|
||||
├─ ⚠️ Context protection: If file reading ≥50k chars → force cli-explore-agent
|
||||
├─ Context protection: If file reading ≥50k chars → force cli-explore-agent
|
||||
└─ Decision:
|
||||
├─ needsExploration=true → Launch parallel cli-explore-agents (1-4 based on complexity)
|
||||
└─ needsExploration=false → Skip to Phase 2/3
|
||||
@@ -140,11 +140,17 @@ function selectAngles(taskDescription, count) {
|
||||
|
||||
const selectedAngles = selectAngles(task_description, complexity === 'High' ? 4 : (complexity === 'Medium' ? 3 : 1))
|
||||
|
||||
// Planning strategy determination
|
||||
const planningStrategy = complexity === 'Low'
|
||||
? 'Direct Claude Planning'
|
||||
: 'cli-lite-planning-agent'
|
||||
|
||||
console.log(`
|
||||
## Exploration Plan
|
||||
|
||||
Task Complexity: ${complexity}
|
||||
Selected Angles: ${selectedAngles.join(', ')}
|
||||
Planning Strategy: ${planningStrategy}
|
||||
|
||||
Launching ${selectedAngles.length} parallel explorations...
|
||||
`)
|
||||
@@ -358,10 +364,7 @@ if (dedupedClarifications.length > 0) {
|
||||
```javascript
|
||||
// 分配规则(优先级从高到低):
|
||||
// 1. 用户明确指定:"用 gemini 分析..." → gemini, "codex 实现..." → codex
|
||||
// 2. 任务类型推断:
|
||||
// - 分析|审查|评估|探索 → gemini
|
||||
// - 实现|创建|修改|修复 → codex (复杂) 或 agent (简单)
|
||||
// 3. 默认 → agent
|
||||
// 2. 默认 → agent
|
||||
|
||||
const executorAssignments = {} // { taskId: { executor: 'gemini'|'codex'|'agent', reason: string } }
|
||||
plan.tasks.forEach(task => {
|
||||
|
||||
@@ -124,6 +124,9 @@ Task(subagent_type="cli-execution-agent", run_in_background=false, prompt=`
|
||||
|
||||
## Analysis Steps
|
||||
|
||||
### 0. Load Output Schema (MANDATORY)
|
||||
Execute: cat ~/.claude/workflows/cli-templates/schemas/conflict-resolution-schema.json
|
||||
|
||||
### 1. Load Context
|
||||
- Read existing files from conflict_detection.existing_files
|
||||
- Load plan from .workflow/active/{session_id}/.process/context-package.json
|
||||
@@ -171,123 +174,14 @@ Task(subagent_type="cli-execution-agent", run_in_background=false, prompt=`
|
||||
|
||||
⚠️ Output to conflict-resolution.json (generated in Phase 4)
|
||||
|
||||
Return JSON format for programmatic processing:
|
||||
**Schema Reference**: Execute \`cat ~/.claude/workflows/cli-templates/schemas/conflict-resolution-schema.json\` to get full schema
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"conflicts": [
|
||||
{
|
||||
"id": "CON-001",
|
||||
"brief": "一行中文冲突摘要",
|
||||
"severity": "Critical|High|Medium",
|
||||
"category": "Architecture|API|Data|Dependency|ModuleOverlap",
|
||||
"affected_files": [
|
||||
".workflow/active/{session}/.brainstorm/guidance-specification.md",
|
||||
".workflow/active/{session}/.brainstorm/system-architect/analysis.md"
|
||||
],
|
||||
"description": "详细描述冲突 - 什么不兼容",
|
||||
"impact": {
|
||||
"scope": "影响的模块/组件",
|
||||
"compatibility": "Yes|No|Partial",
|
||||
"migration_required": true|false,
|
||||
"estimated_effort": "人天估计"
|
||||
},
|
||||
"overlap_analysis": {
|
||||
"// NOTE": "仅当 category=ModuleOverlap 时需要此字段",
|
||||
"new_module": {
|
||||
"name": "新模块名称",
|
||||
"scenarios": ["场景1", "场景2", "场景3"],
|
||||
"responsibilities": "职责描述"
|
||||
},
|
||||
"existing_modules": [
|
||||
{
|
||||
"file": "src/existing/module.ts",
|
||||
"name": "现有模块名称",
|
||||
"scenarios": ["场景A", "场景B"],
|
||||
"overlap_scenarios": ["重叠场景1", "重叠场景2"],
|
||||
"responsibilities": "现有模块职责"
|
||||
}
|
||||
]
|
||||
},
|
||||
"strategies": [
|
||||
{
|
||||
"name": "策略名称(中文)",
|
||||
"approach": "实现方法简述",
|
||||
"complexity": "Low|Medium|High",
|
||||
"risk": "Low|Medium|High",
|
||||
"effort": "时间估计",
|
||||
"pros": ["优点1", "优点2"],
|
||||
"cons": ["缺点1", "缺点2"],
|
||||
"clarification_needed": [
|
||||
"// NOTE: 仅当需要用户进一步澄清时需要此字段(尤其是 ModuleOverlap)",
|
||||
"新模块的核心职责边界是什么?",
|
||||
"如何与现有模块 X 协作?",
|
||||
"哪些场景应该由新模块处理?"
|
||||
],
|
||||
"modifications": [
|
||||
{
|
||||
"file": ".workflow/active/{session}/.brainstorm/guidance-specification.md",
|
||||
"section": "## 2. System Architect Decisions",
|
||||
"change_type": "update",
|
||||
"old_content": "原始内容片段(用于定位)",
|
||||
"new_content": "修改后的内容",
|
||||
"rationale": "为什么这样改"
|
||||
},
|
||||
{
|
||||
"file": ".workflow/active/{session}/.brainstorm/system-architect/analysis.md",
|
||||
"section": "## Design Decisions",
|
||||
"change_type": "update",
|
||||
"old_content": "原始内容片段",
|
||||
"new_content": "修改后的内容",
|
||||
"rationale": "修改理由"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "策略2名称",
|
||||
"approach": "...",
|
||||
"complexity": "Medium",
|
||||
"risk": "Low",
|
||||
"effort": "1-2天",
|
||||
"pros": ["优点"],
|
||||
"cons": ["缺点"],
|
||||
"modifications": [...]
|
||||
}
|
||||
],
|
||||
"recommended": 0,
|
||||
"modification_suggestions": [
|
||||
"建议1:具体的修改方向或注意事项",
|
||||
"建议2:可能需要考虑的边界情况",
|
||||
"建议3:相关的最佳实践或模式"
|
||||
]
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 2,
|
||||
"critical": 1,
|
||||
"high": 1,
|
||||
"medium": 0
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
⚠️ CRITICAL Requirements for modifications field:
|
||||
- old_content: Must be exact text from target file (20-100 chars for unique match)
|
||||
- new_content: Complete replacement text (maintains formatting)
|
||||
- change_type: "update" (replace), "add" (insert), "remove" (delete)
|
||||
- file: Full path relative to project root
|
||||
- section: Markdown heading for context (helps locate position)
|
||||
Return JSON following the schema above. Key requirements:
|
||||
- Minimum 2 strategies per conflict, max 4
|
||||
- All text in Chinese for user-facing fields (brief, name, pros, cons)
|
||||
- modification_suggestions: 2-5 actionable suggestions for custom handling (Chinese)
|
||||
|
||||
Quality Standards:
|
||||
- Each strategy must have actionable modifications
|
||||
- old_content must be precise enough for Edit tool matching
|
||||
- new_content preserves markdown formatting and structure
|
||||
- Recommended strategy (index) based on lowest complexity + risk
|
||||
- modification_suggestions must be specific, actionable, and context-aware
|
||||
- Each suggestion should address a specific aspect (compatibility, migration, testing, etc.)
|
||||
- All text in Chinese for user-facing fields (brief, name, pros, cons, modification_suggestions)
|
||||
- modifications.old_content: 20-100 chars for unique Edit tool matching
|
||||
- modifications.new_content: preserves markdown formatting
|
||||
- modification_suggestions: 2-5 actionable suggestions for custom handling
|
||||
`)
|
||||
```
|
||||
|
||||
@@ -312,143 +206,85 @@ Task(subagent_type="cli-execution-agent", run_in_background=false, prompt=`
|
||||
8. Return execution log path
|
||||
```
|
||||
|
||||
### Phase 3: Iterative User Interaction with Clarification Loop
|
||||
### Phase 3: User Interaction Loop
|
||||
|
||||
**Execution Flow**:
|
||||
```
|
||||
FOR each conflict (逐个处理,无数量限制):
|
||||
clarified = false
|
||||
round = 0
|
||||
userClarifications = []
|
||||
```javascript
|
||||
FOR each conflict:
|
||||
round = 0, clarified = false, userClarifications = []
|
||||
|
||||
WHILE (!clarified && round < 10):
|
||||
round++
|
||||
WHILE (!clarified && round++ < 10):
|
||||
// 1. Display conflict info (text output for context)
|
||||
displayConflictSummary(conflict) // id, brief, severity, overlap_analysis if ModuleOverlap
|
||||
|
||||
// 1. Display conflict (包含所有关键字段)
|
||||
- category, id, brief, severity, description
|
||||
- IF ModuleOverlap: 展示 overlap_analysis
|
||||
* new_module: {name, scenarios, responsibilities}
|
||||
* existing_modules[]: {file, name, scenarios, overlap_scenarios, responsibilities}
|
||||
// 2. Strategy selection via AskUserQuestion
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: formatStrategiesForDisplay(conflict.strategies),
|
||||
header: "策略选择",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
...conflict.strategies.map((s, i) => ({
|
||||
label: `${s.name}${i === conflict.recommended ? ' (推荐)' : ''}`,
|
||||
description: `${s.complexity}复杂度 | ${s.risk}风险${s.clarification_needed?.length ? ' | ⚠️需澄清' : ''}`
|
||||
})),
|
||||
{ label: "自定义修改", description: `建议: ${conflict.modification_suggestions?.slice(0,2).join('; ')}` }
|
||||
]
|
||||
}]
|
||||
})
|
||||
|
||||
// 2. Display strategies (2-4个策略 + 自定义选项)
|
||||
- FOR each strategy: {name, approach, complexity, risk, effort, pros, cons}
|
||||
* IF clarification_needed: 展示待澄清问题列表
|
||||
- 自定义选项: {suggestions: modification_suggestions[]}
|
||||
// 3. Handle selection
|
||||
if (userChoice === "自定义修改") {
|
||||
customConflicts.push({ id, brief, category, suggestions, overlap_analysis })
|
||||
break
|
||||
}
|
||||
|
||||
// 3. User selects strategy
|
||||
userChoice = readInput()
|
||||
selectedStrategy = findStrategyByName(userChoice)
|
||||
|
||||
IF userChoice == "自定义":
|
||||
customConflicts.push({id, brief, category, suggestions, overlap_analysis})
|
||||
clarified = true
|
||||
BREAK
|
||||
// 4. Clarification (if needed) - batched max 4 per call
|
||||
if (selectedStrategy.clarification_needed?.length > 0) {
|
||||
for (batch of chunk(selectedStrategy.clarification_needed, 4)) {
|
||||
AskUserQuestion({
|
||||
questions: batch.map((q, i) => ({
|
||||
question: q, header: `澄清${i+1}`, multiSelect: false,
|
||||
options: [{ label: "详细说明", description: "提供答案" }]
|
||||
}))
|
||||
})
|
||||
userClarifications.push(...collectAnswers(batch))
|
||||
}
|
||||
|
||||
selectedStrategy = strategies[userChoice]
|
||||
|
||||
// 4. Clarification loop
|
||||
IF selectedStrategy.clarification_needed.length > 0:
|
||||
// 收集澄清答案
|
||||
FOR each question:
|
||||
answer = readInput()
|
||||
userClarifications.push({question, answer})
|
||||
|
||||
// Agent 重新分析
|
||||
reanalysisResult = Task(cli-execution-agent, prompt={
|
||||
冲突信息: {id, brief, category, 策略}
|
||||
用户澄清: userClarifications[]
|
||||
场景分析: overlap_analysis (if ModuleOverlap)
|
||||
|
||||
输出: {
|
||||
uniqueness_confirmed: bool,
|
||||
rationale: string,
|
||||
updated_strategy: {name, approach, complexity, risk, effort, modifications[]},
|
||||
remaining_questions: [] (如果仍有歧义)
|
||||
}
|
||||
// 5. Agent re-analysis
|
||||
reanalysisResult = Task({
|
||||
subagent_type: "cli-execution-agent",
|
||||
run_in_background: false,
|
||||
prompt: `Conflict: ${conflict.id}, Strategy: ${selectedStrategy.name}
|
||||
User Clarifications: ${JSON.stringify(userClarifications)}
|
||||
Output: { uniqueness_confirmed, rationale, updated_strategy, remaining_questions }`
|
||||
})
|
||||
|
||||
IF reanalysisResult.uniqueness_confirmed:
|
||||
selectedStrategy = updated_strategy
|
||||
selectedStrategy.clarifications = userClarifications
|
||||
if (reanalysisResult.uniqueness_confirmed) {
|
||||
selectedStrategy = { ...reanalysisResult.updated_strategy, clarifications: userClarifications }
|
||||
clarified = true
|
||||
ELSE:
|
||||
// 更新澄清问题,继续下一轮
|
||||
selectedStrategy.clarification_needed = remaining_questions
|
||||
ELSE:
|
||||
} else {
|
||||
selectedStrategy.clarification_needed = reanalysisResult.remaining_questions
|
||||
}
|
||||
} else {
|
||||
clarified = true
|
||||
}
|
||||
|
||||
resolvedConflicts.push({conflict, strategy: selectedStrategy})
|
||||
if (clarified) resolvedConflicts.push({ conflict, strategy: selectedStrategy })
|
||||
END WHILE
|
||||
END FOR
|
||||
|
||||
// Build output
|
||||
selectedStrategies = resolvedConflicts.map(r => ({
|
||||
conflict_id, strategy, clarifications[]
|
||||
conflict_id: r.conflict.id, strategy: r.strategy, clarifications: r.strategy.clarifications || []
|
||||
}))
|
||||
```
|
||||
|
||||
**Key Data Structures**:
|
||||
|
||||
```javascript
|
||||
// Custom conflict tracking
|
||||
customConflicts[] = {
|
||||
id, brief, category,
|
||||
suggestions: modification_suggestions[],
|
||||
overlap_analysis: { new_module{}, existing_modules[] } // ModuleOverlap only
|
||||
}
|
||||
|
||||
// Agent re-analysis prompt output
|
||||
{
|
||||
uniqueness_confirmed: bool,
|
||||
rationale: string,
|
||||
updated_strategy: {
|
||||
name, approach, complexity, risk, effort,
|
||||
modifications: [{file, section, change_type, old_content, new_content, rationale}]
|
||||
},
|
||||
remaining_questions: string[]
|
||||
}
|
||||
```
|
||||
|
||||
**Text Output Example** (展示关键字段):
|
||||
|
||||
```markdown
|
||||
============================================================
|
||||
冲突 1/3 - 第 1 轮
|
||||
============================================================
|
||||
【ModuleOverlap】CON-001: 新增用户认证服务与现有模块功能重叠
|
||||
严重程度: High | 描述: 计划中的 UserAuthService 与现有 AuthManager 场景重叠
|
||||
|
||||
--- 场景重叠分析 ---
|
||||
新模块: UserAuthService | 场景: 登录, Token验证, 权限, MFA
|
||||
现有模块: AuthManager (src/auth/AuthManager.ts) | 重叠: 登录, Token验证
|
||||
|
||||
--- 解决策略 ---
|
||||
1) 合并 (Low复杂度 | Low风险 | 2-3天)
|
||||
⚠️ 需澄清: AuthManager是否能承担MFA?
|
||||
|
||||
2) 拆分边界 (Medium复杂度 | Medium风险 | 4-5天)
|
||||
⚠️ 需澄清: 基础/高级认证边界? Token验证归谁?
|
||||
|
||||
3) 自定义修改
|
||||
建议: 评估扩展性; 策略模式分离; 定义接口边界
|
||||
|
||||
请选择 (1-3): > 2
|
||||
|
||||
--- 澄清问答 (第1轮) ---
|
||||
Q: 基础/高级认证边界?
|
||||
A: 基础=密码登录+token验证, 高级=MFA+OAuth+SSO
|
||||
|
||||
Q: Token验证归谁?
|
||||
A: 统一由 AuthManager 负责
|
||||
|
||||
🔄 重新分析...
|
||||
✅ 唯一性已确认 | 理由: 边界清晰 - AuthManager(基础+token), UserAuthService(MFA+OAuth+SSO)
|
||||
|
||||
============================================================
|
||||
冲突 2/3 - 第 1 轮 [下一个冲突]
|
||||
============================================================
|
||||
```
|
||||
|
||||
**Loop Characteristics**: 逐个处理 | 无限轮次(max 10) | 动态问题生成 | Agent重新分析判断唯一性 | ModuleOverlap场景边界澄清
|
||||
**Key Points**:
|
||||
- AskUserQuestion: max 4 questions/call, batch if more
|
||||
- Strategy options: 2-4 strategies + "自定义修改"
|
||||
- Clarification loop: max 10 rounds, agent判断 uniqueness_confirmed
|
||||
- Custom conflicts: 记录 overlap_analysis 供后续手动处理
|
||||
|
||||
### Phase 4: Apply Modifications
|
||||
|
||||
|
||||
@@ -354,19 +354,20 @@ Generate task JSON files for ${module.name} module within workflow session
|
||||
IMPORTANT: This is PLANNING ONLY - generate task JSONs, NOT implementing code.
|
||||
IMPORTANT: Generate Task JSONs ONLY. IMPL_PLAN.md and TODO_LIST.md by Phase 3 Coordinator.
|
||||
|
||||
CRITICAL: Follow progressive loading strategy in agent specification
|
||||
CRITICAL: Follow the progressive loading strategy defined in agent specification (load analysis.md files incrementally due to file size)
|
||||
|
||||
## MODULE SCOPE
|
||||
- Module: ${module.name} (${module.type})
|
||||
- Focus Paths: ${module.paths.join(', ')}
|
||||
- Task ID Prefix: IMPL-${module.prefix}
|
||||
- Task Limit: ≤9 tasks
|
||||
- Other Modules: ${otherModules.join(', ')}
|
||||
- Task Limit: ≤9 tasks (hard limit for this module)
|
||||
- Other Modules: ${otherModules.join(', ')} (reference only, do NOT generate tasks for them)
|
||||
|
||||
## SESSION PATHS
|
||||
Input:
|
||||
- Session Metadata: .workflow/active/{session-id}/workflow-session.json
|
||||
- Context Package: .workflow/active/{session-id}/.process/context-package.json
|
||||
|
||||
Output:
|
||||
- Task Dir: .workflow/active/{session-id}/.task/
|
||||
|
||||
@@ -374,21 +375,93 @@ Output:
|
||||
Session ID: {session-id}
|
||||
MCP Capabilities: {exa_code, exa_web, code_index}
|
||||
|
||||
## USER CONFIGURATION (from Phase 0)
|
||||
Execution Method: ${userConfig.executionMethod} // agent|hybrid|cli
|
||||
Preferred CLI Tool: ${userConfig.preferredCliTool} // codex|gemini|qwen|auto
|
||||
Supplementary Materials: ${userConfig.supplementaryMaterials}
|
||||
|
||||
## CLI TOOL SELECTION
|
||||
Based on userConfig.executionMethod:
|
||||
- "agent": No command field in implementation_approach steps
|
||||
- "hybrid": Add command field to complex steps only (agent handles simple steps)
|
||||
- "cli": Add command field to ALL implementation_approach steps
|
||||
|
||||
CLI Resume Support (MANDATORY for all CLI commands):
|
||||
- Use --resume parameter to continue from previous task execution
|
||||
- Read previous task's cliExecutionId from session state
|
||||
- Format: ccw cli -p "[prompt]" --resume ${previousCliId} --tool ${tool} --mode write
|
||||
|
||||
## EXPLORATION CONTEXT (from context-package.exploration_results)
|
||||
- Load exploration_results from context-package.json
|
||||
- Filter for ${module.name} module: Use aggregated_insights.critical_files matching ${module.paths.join(', ')}
|
||||
- Apply module-relevant constraints from aggregated_insights.constraints
|
||||
- Reference aggregated_insights.all_patterns applicable to ${module.name}
|
||||
- Use aggregated_insights.all_integration_points for precise modification locations within module scope
|
||||
- Use conflict_indicators for risk-aware task sequencing
|
||||
|
||||
## CONFLICT RESOLUTION CONTEXT (if exists)
|
||||
- Check context-package.conflict_detection.resolution_file for conflict-resolution.json path
|
||||
- If exists, load .process/conflict-resolution.json:
|
||||
- Apply planning_constraints relevant to ${module.name} as task constraints
|
||||
- Reference resolved_conflicts affecting ${module.name} for implementation approach alignment
|
||||
- Handle custom_conflicts with explicit task notes
|
||||
|
||||
## CROSS-MODULE DEPENDENCIES
|
||||
- Use placeholder: depends_on: ["CROSS::{module}::{pattern}"]
|
||||
- Example: depends_on: ["CROSS::B::api-endpoint"]
|
||||
- For dependencies ON other modules: Use placeholder depends_on: ["CROSS::{module}::{pattern}"]
|
||||
- Example: depends_on: ["CROSS::B::api-endpoint"] (this module depends on B's api-endpoint task)
|
||||
- Phase 3 Coordinator resolves to actual task IDs
|
||||
- For dependencies FROM other modules: Document in task context as "provides_for" annotation
|
||||
|
||||
## EXPECTED DELIVERABLES
|
||||
Task JSON Files (.task/IMPL-${module.prefix}*.json):
|
||||
- 6-field schema per agent specification
|
||||
- 6-field schema (id, title, status, context_package_path, meta, context, flow_control)
|
||||
- Task ID format: IMPL-${module.prefix}1, IMPL-${module.prefix}2, ...
|
||||
- Quantified requirements with explicit counts
|
||||
- Artifacts integration from context package (filtered for ${module.name})
|
||||
- **focus_paths enhanced with exploration critical_files (module-scoped)**
|
||||
- Flow control with pre_analysis steps (include exploration integration_points analysis)
|
||||
- **CLI Execution IDs and strategies (MANDATORY)**
|
||||
- Focus ONLY on ${module.name} module scope
|
||||
|
||||
## CLI EXECUTION ID REQUIREMENTS (MANDATORY)
|
||||
Each task JSON MUST include:
|
||||
- **cli_execution_id**: Unique ID for CLI execution (format: `{session_id}-IMPL-${module.prefix}{seq}`)
|
||||
- **cli_execution**: Strategy object based on depends_on:
|
||||
- No deps → `{ "strategy": "new" }`
|
||||
- 1 dep (single child) → `{ "strategy": "resume", "resume_from": "parent-cli-id" }`
|
||||
- 1 dep (multiple children) → `{ "strategy": "fork", "resume_from": "parent-cli-id" }`
|
||||
- N deps → `{ "strategy": "merge_fork", "merge_from": ["id1", "id2", ...] }`
|
||||
- Cross-module dep → `{ "strategy": "cross_module_fork", "resume_from": "CROSS::{module}::{pattern}" }`
|
||||
|
||||
**CLI Execution Strategy Rules**:
|
||||
1. **new**: Task has no dependencies - starts fresh CLI conversation
|
||||
2. **resume**: Task has 1 parent AND that parent has only this child - continues same conversation
|
||||
3. **fork**: Task has 1 parent BUT parent has multiple children - creates new branch with parent context
|
||||
4. **merge_fork**: Task has multiple parents - merges all parent contexts into new conversation
|
||||
5. **cross_module_fork**: Task depends on task from another module - Phase 3 resolves placeholder
|
||||
|
||||
**Execution Command Patterns**:
|
||||
- new: `ccw cli -p "[prompt]" --tool [tool] --mode write --id [cli_execution_id]`
|
||||
- resume: `ccw cli -p "[prompt]" --resume [resume_from] --tool [tool] --mode write`
|
||||
- fork: `ccw cli -p "[prompt]" --resume [resume_from] --id [cli_execution_id] --tool [tool] --mode write`
|
||||
- merge_fork: `ccw cli -p "[prompt]" --resume [merge_from.join(',')] --id [cli_execution_id] --tool [tool] --mode write`
|
||||
- cross_module_fork: (Phase 3 resolves placeholder, then uses fork pattern)
|
||||
|
||||
## QUALITY STANDARDS
|
||||
Hard Constraints:
|
||||
- Task count <= 9 for this module (hard limit - coordinate with Phase 3 if exceeded)
|
||||
- All requirements quantified (explicit counts and enumerated lists)
|
||||
- Acceptance criteria measurable (include verification commands)
|
||||
- Artifact references mapped from context package (module-scoped filter)
|
||||
- Focus paths use absolute paths or clear relative paths from project root
|
||||
- Cross-module dependencies use CROSS:: placeholder format
|
||||
|
||||
## SUCCESS CRITERIA
|
||||
- Task JSONs saved to .task/ with IMPL-${module.prefix}* naming
|
||||
- Cross-module dependencies use CROSS:: placeholder format
|
||||
- Return task count and brief summary
|
||||
- All task JSONs include cli_execution_id and cli_execution strategy
|
||||
- Cross-module dependencies use CROSS:: placeholder format consistently
|
||||
- Focus paths scoped to ${module.paths.join(', ')} only
|
||||
- Return: task count, task IDs, dependency summary (internal + cross-module)
|
||||
`
|
||||
)
|
||||
);
|
||||
|
||||
584
.claude/skills/_shared/mermaid-utils.md
Normal file
584
.claude/skills/_shared/mermaid-utils.md
Normal file
@@ -0,0 +1,584 @@
|
||||
# Mermaid Utilities Library
|
||||
|
||||
Shared utilities for generating and validating Mermaid diagrams across all analysis skills.
|
||||
|
||||
## Sanitization Functions
|
||||
|
||||
### sanitizeId
|
||||
|
||||
Convert any text to a valid Mermaid node ID.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Sanitize text to valid Mermaid node ID
|
||||
* - Only alphanumeric and underscore allowed
|
||||
* - Cannot start with number
|
||||
* - Truncates to 50 chars max
|
||||
*
|
||||
* @param {string} text - Input text
|
||||
* @returns {string} - Valid Mermaid ID
|
||||
*/
|
||||
function sanitizeId(text) {
|
||||
if (!text) return '_empty';
|
||||
return text
|
||||
.replace(/[^a-zA-Z0-9_\u4e00-\u9fa5]/g, '_') // Allow Chinese chars
|
||||
.replace(/^[0-9]/, '_$&') // Prefix number with _
|
||||
.replace(/_+/g, '_') // Collapse multiple _
|
||||
.substring(0, 50); // Limit length
|
||||
}
|
||||
|
||||
// Examples:
|
||||
// sanitizeId("User-Service") → "User_Service"
|
||||
// sanitizeId("3rdParty") → "_3rdParty"
|
||||
// sanitizeId("用户服务") → "用户服务"
|
||||
```
|
||||
|
||||
### escapeLabel
|
||||
|
||||
Escape special characters for Mermaid labels.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Escape special characters in Mermaid labels
|
||||
* Uses HTML entity encoding for problematic chars
|
||||
*
|
||||
* @param {string} text - Label text
|
||||
* @returns {string} - Escaped label
|
||||
*/
|
||||
function escapeLabel(text) {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.replace(/"/g, "'") // Avoid quote issues
|
||||
.replace(/\(/g, '#40;') // (
|
||||
.replace(/\)/g, '#41;') // )
|
||||
.replace(/\{/g, '#123;') // {
|
||||
.replace(/\}/g, '#125;') // }
|
||||
.replace(/\[/g, '#91;') // [
|
||||
.replace(/\]/g, '#93;') // ]
|
||||
.replace(/</g, '#60;') // <
|
||||
.replace(/>/g, '#62;') // >
|
||||
.replace(/\|/g, '#124;') // |
|
||||
.substring(0, 80); // Limit length
|
||||
}
|
||||
|
||||
// Examples:
|
||||
// escapeLabel("Process(data)") → "Process#40;data#41;"
|
||||
// escapeLabel("Check {valid?}") → "Check #123;valid?#125;"
|
||||
```
|
||||
|
||||
### sanitizeType
|
||||
|
||||
Sanitize type names for class diagrams.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Sanitize type names for Mermaid classDiagram
|
||||
* Removes generics syntax that causes issues
|
||||
*
|
||||
* @param {string} type - Type name
|
||||
* @returns {string} - Sanitized type
|
||||
*/
|
||||
function sanitizeType(type) {
|
||||
if (!type) return 'any';
|
||||
return type
|
||||
.replace(/<[^>]*>/g, '') // Remove generics <T>
|
||||
.replace(/\|/g, ' or ') // Union types
|
||||
.replace(/&/g, ' and ') // Intersection types
|
||||
.replace(/\[\]/g, 'Array') // Array notation
|
||||
.substring(0, 30);
|
||||
}
|
||||
|
||||
// Examples:
|
||||
// sanitizeType("Array<string>") → "Array"
|
||||
// sanitizeType("string | number") → "string or number"
|
||||
```
|
||||
|
||||
## Diagram Generation Functions
|
||||
|
||||
### generateFlowchartNode
|
||||
|
||||
Generate a flowchart node with proper shape.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Generate flowchart node with shape
|
||||
*
|
||||
* @param {string} id - Node ID
|
||||
* @param {string} label - Display label
|
||||
* @param {string} type - Node type: start|end|process|decision|io|subroutine
|
||||
* @returns {string} - Mermaid node definition
|
||||
*/
|
||||
function generateFlowchartNode(id, label, type = 'process') {
|
||||
const safeId = sanitizeId(id);
|
||||
const safeLabel = escapeLabel(label);
|
||||
|
||||
const shapes = {
|
||||
start: `${safeId}(["${safeLabel}"])`, // Stadium shape
|
||||
end: `${safeId}(["${safeLabel}"])`, // Stadium shape
|
||||
process: `${safeId}["${safeLabel}"]`, // Rectangle
|
||||
decision: `${safeId}{"${safeLabel}"}`, // Diamond
|
||||
io: `${safeId}[/"${safeLabel}"/]`, // Parallelogram
|
||||
subroutine: `${safeId}[["${safeLabel}"]]`, // Subroutine
|
||||
database: `${safeId}[("${safeLabel}")]`, // Cylinder
|
||||
manual: `${safeId}[/"${safeLabel}"\\]` // Trapezoid
|
||||
};
|
||||
|
||||
return shapes[type] || shapes.process;
|
||||
}
|
||||
```
|
||||
|
||||
### generateFlowchartEdge
|
||||
|
||||
Generate a flowchart edge with optional label.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Generate flowchart edge
|
||||
*
|
||||
* @param {string} from - Source node ID
|
||||
* @param {string} to - Target node ID
|
||||
* @param {string} label - Edge label (optional)
|
||||
* @param {string} style - Edge style: solid|dashed|thick
|
||||
* @returns {string} - Mermaid edge definition
|
||||
*/
|
||||
function generateFlowchartEdge(from, to, label = '', style = 'solid') {
|
||||
const safeFrom = sanitizeId(from);
|
||||
const safeTo = sanitizeId(to);
|
||||
const safeLabel = label ? `|"${escapeLabel(label)}"|` : '';
|
||||
|
||||
const arrows = {
|
||||
solid: '-->',
|
||||
dashed: '-.->',
|
||||
thick: '==>'
|
||||
};
|
||||
|
||||
const arrow = arrows[style] || arrows.solid;
|
||||
return ` ${safeFrom} ${arrow}${safeLabel} ${safeTo}`;
|
||||
}
|
||||
```
|
||||
|
||||
### generateAlgorithmFlowchart (Enhanced)
|
||||
|
||||
Generate algorithm flowchart with branch/loop support.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Generate algorithm flowchart with decision support
|
||||
*
|
||||
* @param {Object} algorithm - Algorithm definition
|
||||
* - name: Algorithm name
|
||||
* - inputs: [{name, type}]
|
||||
* - outputs: [{name, type}]
|
||||
* - steps: [{id, description, type, next: [id], conditions: [text]}]
|
||||
* @returns {string} - Complete Mermaid flowchart
|
||||
*/
|
||||
function generateAlgorithmFlowchart(algorithm) {
|
||||
let mermaid = 'flowchart TD\n';
|
||||
|
||||
// Start node
|
||||
mermaid += ` START(["开始: ${escapeLabel(algorithm.name)}"])\n`;
|
||||
|
||||
// Input node (if has inputs)
|
||||
if (algorithm.inputs?.length > 0) {
|
||||
const inputList = algorithm.inputs.map(i => `${i.name}: ${i.type}`).join(', ');
|
||||
mermaid += ` INPUT[/"输入: ${escapeLabel(inputList)}"/]\n`;
|
||||
mermaid += ` START --> INPUT\n`;
|
||||
}
|
||||
|
||||
// Process nodes
|
||||
const steps = algorithm.steps || [];
|
||||
for (const step of steps) {
|
||||
const nodeId = sanitizeId(step.id || `STEP_${step.step_num}`);
|
||||
|
||||
if (step.type === 'decision') {
|
||||
mermaid += ` ${nodeId}{"${escapeLabel(step.description)}"}\n`;
|
||||
} else if (step.type === 'io') {
|
||||
mermaid += ` ${nodeId}[/"${escapeLabel(step.description)}"/]\n`;
|
||||
} else if (step.type === 'loop_start') {
|
||||
mermaid += ` ${nodeId}[["循环: ${escapeLabel(step.description)}"]]\n`;
|
||||
} else {
|
||||
mermaid += ` ${nodeId}["${escapeLabel(step.description)}"]\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Output node
|
||||
const outputDesc = algorithm.outputs?.map(o => o.name).join(', ') || '结果';
|
||||
mermaid += ` OUTPUT[/"输出: ${escapeLabel(outputDesc)}"/]\n`;
|
||||
mermaid += ` END_(["结束"])\n`;
|
||||
|
||||
// Connect first step to input/start
|
||||
if (steps.length > 0) {
|
||||
const firstStep = sanitizeId(steps[0].id || 'STEP_1');
|
||||
if (algorithm.inputs?.length > 0) {
|
||||
mermaid += ` INPUT --> ${firstStep}\n`;
|
||||
} else {
|
||||
mermaid += ` START --> ${firstStep}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Connect steps based on next array
|
||||
for (const step of steps) {
|
||||
const nodeId = sanitizeId(step.id || `STEP_${step.step_num}`);
|
||||
|
||||
if (step.next && step.next.length > 0) {
|
||||
step.next.forEach((nextId, index) => {
|
||||
const safeNextId = sanitizeId(nextId);
|
||||
const condition = step.conditions?.[index];
|
||||
|
||||
if (condition) {
|
||||
mermaid += ` ${nodeId} -->|"${escapeLabel(condition)}"| ${safeNextId}\n`;
|
||||
} else {
|
||||
mermaid += ` ${nodeId} --> ${safeNextId}\n`;
|
||||
}
|
||||
});
|
||||
} else if (!step.type?.includes('end')) {
|
||||
// Default: connect to next step or output
|
||||
const stepIndex = steps.indexOf(step);
|
||||
if (stepIndex < steps.length - 1) {
|
||||
const nextStep = sanitizeId(steps[stepIndex + 1].id || `STEP_${stepIndex + 2}`);
|
||||
mermaid += ` ${nodeId} --> ${nextStep}\n`;
|
||||
} else {
|
||||
mermaid += ` ${nodeId} --> OUTPUT\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect output to end
|
||||
mermaid += ` OUTPUT --> END_\n`;
|
||||
|
||||
return mermaid;
|
||||
}
|
||||
```
|
||||
|
||||
## Diagram Validation
|
||||
|
||||
### validateMermaidSyntax
|
||||
|
||||
Comprehensive Mermaid syntax validation.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Validate Mermaid diagram syntax
|
||||
*
|
||||
* @param {string} content - Mermaid diagram content
|
||||
* @returns {Object} - {valid: boolean, issues: string[]}
|
||||
*/
|
||||
function validateMermaidSyntax(content) {
|
||||
const issues = [];
|
||||
|
||||
// Check 1: Diagram type declaration
|
||||
if (!content.match(/^(graph|flowchart|classDiagram|sequenceDiagram|stateDiagram|erDiagram|gantt|pie|mindmap)/m)) {
|
||||
issues.push('Missing diagram type declaration');
|
||||
}
|
||||
|
||||
// Check 2: Undefined values
|
||||
if (content.includes('undefined') || content.includes('null')) {
|
||||
issues.push('Contains undefined/null values');
|
||||
}
|
||||
|
||||
// Check 3: Invalid arrow syntax
|
||||
if (content.match(/-->\s*-->/)) {
|
||||
issues.push('Double arrow syntax error');
|
||||
}
|
||||
|
||||
// Check 4: Unescaped special characters in labels
|
||||
const labelMatches = content.match(/\["[^"]*[(){}[\]<>][^"]*"\]/g);
|
||||
if (labelMatches?.some(m => !m.includes('#'))) {
|
||||
issues.push('Unescaped special characters in labels');
|
||||
}
|
||||
|
||||
// Check 5: Node ID starts with number
|
||||
if (content.match(/\n\s*[0-9][a-zA-Z0-9_]*[\[\({]/)) {
|
||||
issues.push('Node ID cannot start with number');
|
||||
}
|
||||
|
||||
// Check 6: Nested subgraph syntax error
|
||||
if (content.match(/subgraph\s+\S+\s*\n[^e]*subgraph/)) {
|
||||
// This is actually valid, only flag if brackets don't match
|
||||
const subgraphCount = (content.match(/subgraph/g) || []).length;
|
||||
const endCount = (content.match(/\bend\b/g) || []).length;
|
||||
if (subgraphCount > endCount) {
|
||||
issues.push('Unbalanced subgraph/end blocks');
|
||||
}
|
||||
}
|
||||
|
||||
// Check 7: Invalid arrow type for diagram type
|
||||
const diagramType = content.match(/^(graph|flowchart|classDiagram|sequenceDiagram)/m)?.[1];
|
||||
if (diagramType === 'classDiagram' && content.includes('-->|')) {
|
||||
issues.push('Invalid edge label syntax for classDiagram');
|
||||
}
|
||||
|
||||
// Check 8: Empty node labels
|
||||
if (content.match(/\[""\]|\{\}|\(\)/)) {
|
||||
issues.push('Empty node labels detected');
|
||||
}
|
||||
|
||||
// Check 9: Reserved keywords as IDs
|
||||
const reserved = ['end', 'graph', 'subgraph', 'direction', 'class', 'click'];
|
||||
for (const keyword of reserved) {
|
||||
const pattern = new RegExp(`\\n\\s*${keyword}\\s*[\\[\\(\\{]`, 'i');
|
||||
if (content.match(pattern)) {
|
||||
issues.push(`Reserved keyword "${keyword}" used as node ID`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check 10: Line length (Mermaid has issues with very long lines)
|
||||
const lines = content.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].length > 500) {
|
||||
issues.push(`Line ${i + 1} exceeds 500 characters`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: issues.length === 0,
|
||||
issues
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### validateDiagramDirectory
|
||||
|
||||
Validate all diagrams in a directory.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Validate all Mermaid diagrams in directory
|
||||
*
|
||||
* @param {string} diagramDir - Path to diagrams directory
|
||||
* @returns {Object[]} - Array of {file, valid, issues}
|
||||
*/
|
||||
function validateDiagramDirectory(diagramDir) {
|
||||
const files = Glob(`${diagramDir}/*.mmd`);
|
||||
const results = [];
|
||||
|
||||
for (const file of files) {
|
||||
const content = Read(file);
|
||||
const validation = validateMermaidSyntax(content);
|
||||
|
||||
results.push({
|
||||
file: file.split('/').pop(),
|
||||
path: file,
|
||||
valid: validation.valid,
|
||||
issues: validation.issues,
|
||||
lines: content.split('\n').length
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
## Class Diagram Utilities
|
||||
|
||||
### generateClassDiagram
|
||||
|
||||
Generate class diagram with relationships.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Generate class diagram from analysis data
|
||||
*
|
||||
* @param {Object} analysis - Data structure analysis
|
||||
* - entities: [{name, type, properties, methods}]
|
||||
* - relationships: [{from, to, type, label}]
|
||||
* @param {Object} options - Generation options
|
||||
* - maxClasses: Max classes to include (default: 15)
|
||||
* - maxProperties: Max properties per class (default: 8)
|
||||
* - maxMethods: Max methods per class (default: 6)
|
||||
* @returns {string} - Mermaid classDiagram
|
||||
*/
|
||||
function generateClassDiagram(analysis, options = {}) {
|
||||
const maxClasses = options.maxClasses || 15;
|
||||
const maxProperties = options.maxProperties || 8;
|
||||
const maxMethods = options.maxMethods || 6;
|
||||
|
||||
let mermaid = 'classDiagram\n';
|
||||
|
||||
const entities = (analysis.entities || []).slice(0, maxClasses);
|
||||
|
||||
// Generate classes
|
||||
for (const entity of entities) {
|
||||
const className = sanitizeId(entity.name);
|
||||
mermaid += ` class ${className} {\n`;
|
||||
|
||||
// Properties
|
||||
for (const prop of (entity.properties || []).slice(0, maxProperties)) {
|
||||
const vis = {public: '+', private: '-', protected: '#'}[prop.visibility] || '+';
|
||||
const type = sanitizeType(prop.type);
|
||||
mermaid += ` ${vis}${type} ${prop.name}\n`;
|
||||
}
|
||||
|
||||
// Methods
|
||||
for (const method of (entity.methods || []).slice(0, maxMethods)) {
|
||||
const vis = {public: '+', private: '-', protected: '#'}[method.visibility] || '+';
|
||||
const params = (method.params || []).map(p => p.name).join(', ');
|
||||
const returnType = sanitizeType(method.returnType || 'void');
|
||||
mermaid += ` ${vis}${method.name}(${params}) ${returnType}\n`;
|
||||
}
|
||||
|
||||
mermaid += ' }\n';
|
||||
|
||||
// Add stereotype if applicable
|
||||
if (entity.type === 'interface') {
|
||||
mermaid += ` <<interface>> ${className}\n`;
|
||||
} else if (entity.type === 'abstract') {
|
||||
mermaid += ` <<abstract>> ${className}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate relationships
|
||||
const arrows = {
|
||||
inheritance: '--|>',
|
||||
implementation: '..|>',
|
||||
composition: '*--',
|
||||
aggregation: 'o--',
|
||||
association: '-->',
|
||||
dependency: '..>'
|
||||
};
|
||||
|
||||
for (const rel of (analysis.relationships || [])) {
|
||||
const from = sanitizeId(rel.from);
|
||||
const to = sanitizeId(rel.to);
|
||||
const arrow = arrows[rel.type] || '-->';
|
||||
const label = rel.label ? ` : ${escapeLabel(rel.label)}` : '';
|
||||
|
||||
// Only include if both entities exist
|
||||
if (entities.some(e => sanitizeId(e.name) === from) &&
|
||||
entities.some(e => sanitizeId(e.name) === to)) {
|
||||
mermaid += ` ${from} ${arrow} ${to}${label}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return mermaid;
|
||||
}
|
||||
```
|
||||
|
||||
## Sequence Diagram Utilities
|
||||
|
||||
### generateSequenceDiagram
|
||||
|
||||
Generate sequence diagram from scenario.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Generate sequence diagram from scenario
|
||||
*
|
||||
* @param {Object} scenario - Sequence scenario
|
||||
* - name: Scenario name
|
||||
* - actors: [{id, name, type}]
|
||||
* - messages: [{from, to, description, type}]
|
||||
* - blocks: [{type, condition, messages}]
|
||||
* @returns {string} - Mermaid sequenceDiagram
|
||||
*/
|
||||
function generateSequenceDiagram(scenario) {
|
||||
let mermaid = 'sequenceDiagram\n';
|
||||
|
||||
// Title
|
||||
if (scenario.name) {
|
||||
mermaid += ` title ${escapeLabel(scenario.name)}\n`;
|
||||
}
|
||||
|
||||
// Participants
|
||||
for (const actor of scenario.actors || []) {
|
||||
const actorType = actor.type === 'external' ? 'actor' : 'participant';
|
||||
mermaid += ` ${actorType} ${sanitizeId(actor.id)} as ${escapeLabel(actor.name)}\n`;
|
||||
}
|
||||
|
||||
mermaid += '\n';
|
||||
|
||||
// Messages
|
||||
for (const msg of scenario.messages || []) {
|
||||
const from = sanitizeId(msg.from);
|
||||
const to = sanitizeId(msg.to);
|
||||
const desc = escapeLabel(msg.description);
|
||||
|
||||
let arrow;
|
||||
switch (msg.type) {
|
||||
case 'async': arrow = '->>'; break;
|
||||
case 'response': arrow = '-->>'; break;
|
||||
case 'create': arrow = '->>+'; break;
|
||||
case 'destroy': arrow = '->>-'; break;
|
||||
case 'self': arrow = '->>'; break;
|
||||
default: arrow = '->>';
|
||||
}
|
||||
|
||||
mermaid += ` ${from}${arrow}${to}: ${desc}\n`;
|
||||
|
||||
// Activation
|
||||
if (msg.activate) {
|
||||
mermaid += ` activate ${to}\n`;
|
||||
}
|
||||
if (msg.deactivate) {
|
||||
mermaid += ` deactivate ${from}\n`;
|
||||
}
|
||||
|
||||
// Notes
|
||||
if (msg.note) {
|
||||
mermaid += ` Note over ${to}: ${escapeLabel(msg.note)}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Blocks (loops, alt, opt)
|
||||
for (const block of scenario.blocks || []) {
|
||||
switch (block.type) {
|
||||
case 'loop':
|
||||
mermaid += ` loop ${escapeLabel(block.condition)}\n`;
|
||||
break;
|
||||
case 'alt':
|
||||
mermaid += ` alt ${escapeLabel(block.condition)}\n`;
|
||||
break;
|
||||
case 'opt':
|
||||
mermaid += ` opt ${escapeLabel(block.condition)}\n`;
|
||||
break;
|
||||
}
|
||||
|
||||
for (const m of block.messages || []) {
|
||||
mermaid += ` ${sanitizeId(m.from)}->>${sanitizeId(m.to)}: ${escapeLabel(m.description)}\n`;
|
||||
}
|
||||
|
||||
mermaid += ' end\n';
|
||||
}
|
||||
|
||||
return mermaid;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Algorithm with Branches
|
||||
|
||||
```javascript
|
||||
const algorithm = {
|
||||
name: "用户认证流程",
|
||||
inputs: [{name: "credentials", type: "Object"}],
|
||||
outputs: [{name: "token", type: "JWT"}],
|
||||
steps: [
|
||||
{id: "validate", description: "验证输入格式", type: "process"},
|
||||
{id: "check_user", description: "用户是否存在?", type: "decision",
|
||||
next: ["verify_pwd", "error_user"], conditions: ["是", "否"]},
|
||||
{id: "verify_pwd", description: "验证密码", type: "process"},
|
||||
{id: "pwd_ok", description: "密码正确?", type: "decision",
|
||||
next: ["gen_token", "error_pwd"], conditions: ["是", "否"]},
|
||||
{id: "gen_token", description: "生成 JWT Token", type: "process"},
|
||||
{id: "error_user", description: "返回用户不存在", type: "io"},
|
||||
{id: "error_pwd", description: "返回密码错误", type: "io"}
|
||||
]
|
||||
};
|
||||
|
||||
const flowchart = generateAlgorithmFlowchart(algorithm);
|
||||
```
|
||||
|
||||
### Example 2: Validate Before Output
|
||||
|
||||
```javascript
|
||||
const diagram = generateClassDiagram(analysis);
|
||||
const validation = validateMermaidSyntax(diagram);
|
||||
|
||||
if (!validation.valid) {
|
||||
console.log("Diagram has issues:", validation.issues);
|
||||
// Fix issues or regenerate
|
||||
} else {
|
||||
Write(`${outputDir}/class-diagram.mmd`, diagram);
|
||||
}
|
||||
```
|
||||
132
.claude/skills/copyright-docs/SKILL.md
Normal file
132
.claude/skills/copyright-docs/SKILL.md
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
name: copyright-docs
|
||||
description: Generate software copyright design specification documents compliant with China Copyright Protection Center (CPCC) standards. Creates complete design documents with Mermaid diagrams based on source code analysis. Use for software copyright registration, generating design specification, creating CPCC-compliant documents, or documenting software for intellectual property protection. Triggers on "软件著作权", "设计说明书", "版权登记", "CPCC", "软著申请".
|
||||
allowed-tools: Task, AskUserQuestion, Read, Bash, Glob, Grep, Write
|
||||
---
|
||||
|
||||
# Software Copyright Documentation Skill
|
||||
|
||||
Generate CPCC-compliant software design specification documents (软件设计说明书) through multi-phase code analysis.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Context-Optimized Architecture │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Phase 1: Metadata → project-metadata.json │
|
||||
│ ↓ │
|
||||
│ Phase 2: 6 Parallel → sections/section-N.md (直接写MD) │
|
||||
│ Agents ↓ 返回简要JSON │
|
||||
│ ↓ │
|
||||
│ Phase 2.5: Consolidation → cross-module-summary.md │
|
||||
│ Agent ↓ 返回问题列表 │
|
||||
│ ↓ │
|
||||
│ Phase 4: Assembly → 合并MD + 跨模块总结 │
|
||||
│ ↓ │
|
||||
│ Phase 5: Refinement → 最终文档 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
1. **Agent 直接输出 MD**: 避免 JSON → MD 转换的上下文开销
|
||||
2. **简要返回**: Agent 只返回路径+摘要,不返回完整内容
|
||||
3. **汇总 Agent**: 独立 Agent 负责跨模块问题检测
|
||||
4. **引用合并**: Phase 4 读取文件合并,不在上下文中传递
|
||||
|
||||
## Execution Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Phase 1: Metadata Collection │
|
||||
│ → Read: phases/01-metadata-collection.md │
|
||||
│ → Collect: software name, version, category, scope │
|
||||
│ → Output: project-metadata.json │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 2: Deep Code Analysis (6 Parallel Agents) │
|
||||
│ → Read: phases/02-deep-analysis.md │
|
||||
│ → Reference: specs/cpcc-requirements.md │
|
||||
│ → Each Agent: 分析代码 → 直接写 sections/section-N.md │
|
||||
│ → Return: {"status", "output_file", "summary", "cross_notes"} │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 2.5: Consolidation (New!) │
|
||||
│ → Read: phases/02.5-consolidation.md │
|
||||
│ → Input: Agent 返回的简要信息 + cross_module_notes │
|
||||
│ → Analyze: 一致性/完整性/关联性/质量检查 │
|
||||
│ → Output: cross-module-summary.md │
|
||||
│ → Return: {"issues": {errors, warnings, info}, "stats"} │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 4: Document Assembly │
|
||||
│ → Read: phases/04-document-assembly.md │
|
||||
│ → Check: 如有 errors,提示用户处理 │
|
||||
│ → Merge: Section 1 + sections/*.md + 跨模块附录 │
|
||||
│ → Output: {软件名称}-软件设计说明书.md │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 5: Compliance Review & Refinement │
|
||||
│ → Read: phases/05-compliance-refinement.md │
|
||||
│ → Reference: specs/cpcc-requirements.md │
|
||||
│ → Loop: 发现问题 → 提问 → 修复 → 重新检查 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Document Sections (7 Required)
|
||||
|
||||
| Section | Title | Diagram | Agent |
|
||||
|---------|-------|---------|-------|
|
||||
| 1 | 软件概述 | - | Phase 4 生成 |
|
||||
| 2 | 系统架构图 | graph TD | architecture |
|
||||
| 3 | 功能模块设计 | flowchart TD | functions |
|
||||
| 4 | 核心算法与流程 | flowchart TD | algorithms |
|
||||
| 5 | 数据结构设计 | classDiagram | data_structures |
|
||||
| 6 | 接口设计 | sequenceDiagram | interfaces |
|
||||
| 7 | 异常处理设计 | flowchart TD | exceptions |
|
||||
|
||||
## Directory Setup
|
||||
|
||||
```javascript
|
||||
// 生成时间戳目录名
|
||||
const timestamp = new Date().toISOString().slice(0,19).replace(/[-:T]/g, '');
|
||||
const dir = `.workflow/.scratchpad/copyright-${timestamp}`;
|
||||
|
||||
// Windows (cmd)
|
||||
Bash(`mkdir "${dir}\\sections"`);
|
||||
Bash(`mkdir "${dir}\\iterations"`);
|
||||
|
||||
// Unix/macOS
|
||||
// Bash(`mkdir -p "${dir}/sections" "${dir}/iterations"`);
|
||||
```
|
||||
|
||||
## Output Structure
|
||||
|
||||
```
|
||||
.workflow/.scratchpad/copyright-{timestamp}/
|
||||
├── project-metadata.json # Phase 1
|
||||
├── sections/ # Phase 2 (Agent 直接写入)
|
||||
│ ├── section-2-architecture.md
|
||||
│ ├── section-3-functions.md
|
||||
│ ├── section-4-algorithms.md
|
||||
│ ├── section-5-data-structures.md
|
||||
│ ├── section-6-interfaces.md
|
||||
│ └── section-7-exceptions.md
|
||||
├── cross-module-summary.md # Phase 2.5
|
||||
├── iterations/ # Phase 5
|
||||
│ ├── v1.md
|
||||
│ └── v2.md
|
||||
└── {软件名称}-软件设计说明书.md # Final Output
|
||||
```
|
||||
|
||||
## Reference Documents
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [phases/01-metadata-collection.md](phases/01-metadata-collection.md) | Software info collection |
|
||||
| [phases/02-deep-analysis.md](phases/02-deep-analysis.md) | 6-agent parallel analysis |
|
||||
| [phases/02.5-consolidation.md](phases/02.5-consolidation.md) | Cross-module consolidation |
|
||||
| [phases/04-document-assembly.md](phases/04-document-assembly.md) | Document merge & assembly |
|
||||
| [phases/05-compliance-refinement.md](phases/05-compliance-refinement.md) | Iterative refinement loop |
|
||||
| [specs/cpcc-requirements.md](specs/cpcc-requirements.md) | CPCC compliance checklist |
|
||||
| [templates/agent-base.md](templates/agent-base.md) | Agent prompt templates |
|
||||
| [../_shared/mermaid-utils.md](../_shared/mermaid-utils.md) | Shared Mermaid utilities |
|
||||
@@ -0,0 +1,78 @@
|
||||
# Phase 1: Metadata Collection
|
||||
|
||||
Collect software metadata for document header and context.
|
||||
|
||||
## Execution
|
||||
|
||||
### Step 1: Software Name & Version
|
||||
|
||||
```javascript
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: "请输入软件名称(将显示在文档页眉):",
|
||||
header: "软件名称",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{label: "自动检测", description: "从 package.json 或项目配置读取"},
|
||||
{label: "手动输入", description: "输入自定义名称"}
|
||||
]
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
### Step 2: Software Category
|
||||
|
||||
```javascript
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: "软件属于哪种类型?",
|
||||
header: "软件类型",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{label: "命令行工具 (CLI)", description: "重点描述命令、参数"},
|
||||
{label: "后端服务/API", description: "重点描述端点、协议"},
|
||||
{label: "SDK/库", description: "重点描述接口、集成"},
|
||||
{label: "数据处理系统", description: "重点描述数据流、转换"},
|
||||
{label: "自动化脚本", description: "重点描述工作流、触发器"}
|
||||
]
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
### Step 3: Scope Definition
|
||||
|
||||
```javascript
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: "分析范围是什么?",
|
||||
header: "分析范围",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{label: "整个项目", description: "分析全部源代码"},
|
||||
{label: "指定目录", description: "仅分析 src/ 或其他目录"},
|
||||
{label: "自定义路径", description: "手动指定路径"}
|
||||
]
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Save metadata to `project-metadata.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"software_name": "智能数据分析系统",
|
||||
"version": "V1.0.0",
|
||||
"category": "后端服务/API",
|
||||
"scope_path": "src/",
|
||||
"tech_stack": {
|
||||
"language": "TypeScript",
|
||||
"runtime": "Node.js 18+",
|
||||
"framework": "Express.js",
|
||||
"dependencies": ["mongoose", "redis", "bull"]
|
||||
},
|
||||
"entry_points": ["src/index.ts", "src/cli.ts"],
|
||||
"main_modules": ["auth", "data", "api", "worker"]
|
||||
}
|
||||
```
|
||||
454
.claude/skills/copyright-docs/phases/02-deep-analysis.md
Normal file
454
.claude/skills/copyright-docs/phases/02-deep-analysis.md
Normal file
@@ -0,0 +1,454 @@
|
||||
# Phase 2: Deep Code Analysis
|
||||
|
||||
6 个并行 Agent,各自直接写入 MD 章节文件。
|
||||
|
||||
> **模板参考**: [../templates/agent-base.md](../templates/agent-base.md)
|
||||
> **规范参考**: [../specs/cpcc-requirements.md](../specs/cpcc-requirements.md)
|
||||
|
||||
## Agent 执行前置条件
|
||||
|
||||
**每个 Agent 必须首先读取以下规范文件**:
|
||||
|
||||
```javascript
|
||||
// Agent 启动时的第一步操作
|
||||
const specs = {
|
||||
cpcc: Read(`${skillRoot}/specs/cpcc-requirements.md`)
|
||||
};
|
||||
```
|
||||
|
||||
规范文件路径(相对于 skill 根目录):
|
||||
- `specs/cpcc-requirements.md` - CPCC 软著申请规范要求
|
||||
|
||||
---
|
||||
|
||||
## Agent 配置
|
||||
|
||||
| Agent | 输出文件 | 章节 |
|
||||
|-------|----------|------|
|
||||
| architecture | section-2-architecture.md | 系统架构图 |
|
||||
| functions | section-3-functions.md | 功能模块设计 |
|
||||
| algorithms | section-4-algorithms.md | 核心算法与流程 |
|
||||
| data_structures | section-5-data-structures.md | 数据结构设计 |
|
||||
| interfaces | section-6-interfaces.md | 接口设计 |
|
||||
| exceptions | section-7-exceptions.md | 异常处理设计 |
|
||||
|
||||
## CPCC 规范要点 (所有 Agent 共用)
|
||||
|
||||
```
|
||||
[CPCC_SPEC]
|
||||
1. 内容基于代码分析,无臆测或未来计划
|
||||
2. 图表编号格式: 图N-M (如图2-1, 图3-1)
|
||||
3. 每个子章节内容不少于100字
|
||||
4. Mermaid 语法必须正确可渲染
|
||||
5. 包含具体文件路径引用
|
||||
6. 中文输出,技术术语可用英文
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
```javascript
|
||||
// 1. 准备目录
|
||||
Bash(`mkdir -p ${outputDir}/sections`);
|
||||
|
||||
// 2. 并行启动 6 个 Agent
|
||||
const results = await Promise.all([
|
||||
launchAgent('architecture', metadata, outputDir),
|
||||
launchAgent('functions', metadata, outputDir),
|
||||
launchAgent('algorithms', metadata, outputDir),
|
||||
launchAgent('data_structures', metadata, outputDir),
|
||||
launchAgent('interfaces', metadata, outputDir),
|
||||
launchAgent('exceptions', metadata, outputDir)
|
||||
]);
|
||||
|
||||
// 3. 收集返回信息
|
||||
const summaries = results.map(r => JSON.parse(r));
|
||||
|
||||
// 4. 传递给 Phase 2.5
|
||||
return { summaries, cross_notes: summaries.flatMap(s => s.cross_module_notes) };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent 提示词
|
||||
|
||||
### Architecture
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/cpcc-requirements.md
|
||||
严格遵循 CPCC 软著申请规范要求。
|
||||
|
||||
[ROLE] 系统架构师,专注于分层设计和模块依赖。
|
||||
|
||||
[TASK]
|
||||
分析 ${meta.scope_path},生成 Section 2: 系统架构图。
|
||||
输出: ${outDir}/sections/section-2-architecture.md
|
||||
|
||||
[CPCC_SPEC]
|
||||
- 内容基于代码分析,无臆测
|
||||
- 图表编号: 图2-1, 图2-2...
|
||||
- 每个子章节 ≥100字
|
||||
- 包含文件路径引用
|
||||
|
||||
[TEMPLATE]
|
||||
## 2. 系统架构图
|
||||
|
||||
本章节展示${meta.software_name}的系统架构设计。
|
||||
|
||||
\`\`\`mermaid
|
||||
graph TD
|
||||
subgraph Layer1["层名"]
|
||||
Comp1[组件1]
|
||||
end
|
||||
Comp1 --> Comp2
|
||||
\`\`\`
|
||||
|
||||
**图2-1 系统架构图**
|
||||
|
||||
### 2.1 分层说明
|
||||
| 层级 | 组件 | 职责 |
|
||||
|------|------|------|
|
||||
|
||||
### 2.2 模块依赖
|
||||
| 模块 | 依赖 | 说明 |
|
||||
|------|------|------|
|
||||
|
||||
[FOCUS]
|
||||
1. 分层: 识别代码层次 (Controller/Service/Repository 或其他)
|
||||
2. 模块: 核心模块及职责边界
|
||||
3. 依赖: 模块间依赖方向
|
||||
4. 数据流: 请求/数据的流动路径
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-2-architecture.md","summary":"<50字摘要>","cross_module_notes":["跨模块发现"],"stats":{"diagrams":1,"subsections":2}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Functions
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/cpcc-requirements.md
|
||||
严格遵循 CPCC 软著申请规范要求。
|
||||
|
||||
[ROLE] 功能分析师,专注于功能点识别和交互。
|
||||
|
||||
[TASK]
|
||||
分析 ${meta.scope_path},生成 Section 3: 功能模块设计。
|
||||
输出: ${outDir}/sections/section-3-functions.md
|
||||
|
||||
[CPCC_SPEC]
|
||||
- 内容基于代码分析,无臆测
|
||||
- 图表编号: 图3-1, 图3-2...
|
||||
- 每个子章节 ≥100字
|
||||
- 包含文件路径引用
|
||||
|
||||
[TEMPLATE]
|
||||
## 3. 功能模块设计
|
||||
|
||||
本章节展示${meta.software_name}的功能模块结构。
|
||||
|
||||
\`\`\`mermaid
|
||||
flowchart TD
|
||||
ROOT["${meta.software_name}"]
|
||||
subgraph Group1["模块组1"]
|
||||
F1["功能1"]
|
||||
end
|
||||
ROOT --> Group1
|
||||
\`\`\`
|
||||
|
||||
**图3-1 功能模块结构图**
|
||||
|
||||
### 3.1 功能清单
|
||||
| ID | 功能名称 | 模块 | 入口文件 | 说明 |
|
||||
|----|----------|------|----------|------|
|
||||
|
||||
### 3.2 功能交互
|
||||
| 调用方 | 被调用方 | 触发条件 |
|
||||
|--------|----------|----------|
|
||||
|
||||
[FOCUS]
|
||||
1. 功能点: 枚举所有用户可见功能
|
||||
2. 模块分组: 按业务域分组
|
||||
3. 入口: 每个功能的代码入口 \`src/path/file.ts\`
|
||||
4. 交互: 功能间的调用关系
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-3-functions.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Algorithms
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/cpcc-requirements.md
|
||||
严格遵循 CPCC 软著申请规范要求。
|
||||
|
||||
[ROLE] 算法工程师,专注于核心逻辑和复杂度分析。
|
||||
|
||||
[TASK]
|
||||
分析 ${meta.scope_path},生成 Section 4: 核心算法与流程。
|
||||
输出: ${outDir}/sections/section-4-algorithms.md
|
||||
|
||||
[CPCC_SPEC]
|
||||
- 内容基于代码分析,无臆测
|
||||
- 图表编号: 图4-1, 图4-2... (每个算法一个流程图)
|
||||
- 每个算法说明 ≥100字
|
||||
- 包含文件路径和行号引用
|
||||
|
||||
[TEMPLATE]
|
||||
## 4. 核心算法与流程
|
||||
|
||||
本章节展示${meta.software_name}的核心算法设计。
|
||||
|
||||
### 4.1 {算法名称}
|
||||
|
||||
**说明**: {描述,≥100字}
|
||||
**位置**: \`src/path/file.ts:line\`
|
||||
|
||||
**输入**: param1 (type) - 说明
|
||||
**输出**: result (type) - 说明
|
||||
|
||||
\`\`\`mermaid
|
||||
flowchart TD
|
||||
Start([开始]) --> Input[/输入/]
|
||||
Input --> Check{判断}
|
||||
Check -->|是| P1[步骤1]
|
||||
Check -->|否| P2[步骤2]
|
||||
P1 --> End([结束])
|
||||
P2 --> End
|
||||
\`\`\`
|
||||
|
||||
**图4-1 {算法名称}流程图**
|
||||
|
||||
### 4.N 复杂度分析
|
||||
| 算法 | 时间 | 空间 | 文件 |
|
||||
|------|------|------|------|
|
||||
|
||||
[FOCUS]
|
||||
1. 核心算法: 业务逻辑的关键算法 (>10行或含分支循环)
|
||||
2. 流程步骤: 分支/循环/条件逻辑
|
||||
3. 复杂度: 时间/空间复杂度估算
|
||||
4. 输入输出: 参数类型和返回值
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-4-algorithms.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Data Structures
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/cpcc-requirements.md
|
||||
严格遵循 CPCC 软著申请规范要求。
|
||||
|
||||
[ROLE] 数据建模师,专注于实体关系和类型定义。
|
||||
|
||||
[TASK]
|
||||
分析 ${meta.scope_path},生成 Section 5: 数据结构设计。
|
||||
输出: ${outDir}/sections/section-5-data-structures.md
|
||||
|
||||
[CPCC_SPEC]
|
||||
- 内容基于代码分析,无臆测
|
||||
- 图表编号: 图5-1 (数据结构类图)
|
||||
- 每个子章节 ≥100字
|
||||
- 包含文件路径引用
|
||||
|
||||
[TEMPLATE]
|
||||
## 5. 数据结构设计
|
||||
|
||||
本章节展示${meta.software_name}的核心数据结构。
|
||||
|
||||
\`\`\`mermaid
|
||||
classDiagram
|
||||
class Entity1 {
|
||||
+type field1
|
||||
+method1()
|
||||
}
|
||||
Entity1 "1" --> "*" Entity2 : 关系
|
||||
\`\`\`
|
||||
|
||||
**图5-1 数据结构类图**
|
||||
|
||||
### 5.1 实体说明
|
||||
| 实体 | 类型 | 文件 | 说明 |
|
||||
|------|------|------|------|
|
||||
|
||||
### 5.2 关系说明
|
||||
| 源 | 目标 | 类型 | 基数 |
|
||||
|----|------|------|------|
|
||||
|
||||
[FOCUS]
|
||||
1. 实体: class/interface/type 定义
|
||||
2. 属性: 字段类型和可见性 (+public/-private/#protected)
|
||||
3. 关系: 继承(--|>)/组合(*--)/关联(-->)
|
||||
4. 枚举: enum 类型及其值
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-5-data-structures.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Interfaces
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/cpcc-requirements.md
|
||||
严格遵循 CPCC 软著申请规范要求。
|
||||
|
||||
[ROLE] API设计师,专注于接口契约和协议。
|
||||
|
||||
[TASK]
|
||||
分析 ${meta.scope_path},生成 Section 6: 接口设计。
|
||||
输出: ${outDir}/sections/section-6-interfaces.md
|
||||
|
||||
[CPCC_SPEC]
|
||||
- 内容基于代码分析,无臆测
|
||||
- 图表编号: 图6-1, 图6-2... (每个核心接口一个时序图)
|
||||
- 每个接口详情 ≥100字
|
||||
- 包含文件路径引用
|
||||
|
||||
[TEMPLATE]
|
||||
## 6. 接口设计
|
||||
|
||||
本章节展示${meta.software_name}的接口设计。
|
||||
|
||||
\`\`\`mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant A as API
|
||||
participant S as Service
|
||||
C->>A: POST /api/xxx
|
||||
A->>S: method()
|
||||
S-->>A: result
|
||||
A-->>C: 200 OK
|
||||
\`\`\`
|
||||
|
||||
**图6-1 {接口名}时序图**
|
||||
|
||||
### 6.1 接口清单
|
||||
| 接口 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
|
||||
### 6.2 接口详情
|
||||
|
||||
#### METHOD /path
|
||||
**请求**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
|
||||
**响应**:
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
|
||||
[FOCUS]
|
||||
1. API端点: 路径/方法/说明
|
||||
2. 参数: 请求参数类型和校验规则
|
||||
3. 响应: 响应格式、状态码、错误码
|
||||
4. 时序: 典型调用流程 (选2-3个核心接口)
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-6-interfaces.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Exceptions
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/cpcc-requirements.md
|
||||
严格遵循 CPCC 软著申请规范要求。
|
||||
|
||||
[ROLE] 可靠性工程师,专注于异常处理和恢复策略。
|
||||
|
||||
[TASK]
|
||||
分析 ${meta.scope_path},生成 Section 7: 异常处理设计。
|
||||
输出: ${outDir}/sections/section-7-exceptions.md
|
||||
|
||||
[CPCC_SPEC]
|
||||
- 内容基于代码分析,无臆测
|
||||
- 图表编号: 图7-1 (异常处理流程图)
|
||||
- 每个子章节 ≥100字
|
||||
- 包含文件路径引用
|
||||
|
||||
[TEMPLATE]
|
||||
## 7. 异常处理设计
|
||||
|
||||
本章节展示${meta.software_name}的异常处理机制。
|
||||
|
||||
\`\`\`mermaid
|
||||
flowchart TD
|
||||
Req[请求] --> Try{Try-Catch}
|
||||
Try -->|正常| Process[处理]
|
||||
Try -->|异常| ErrType{类型}
|
||||
ErrType -->|E1| H1[处理1]
|
||||
ErrType -->|E2| H2[处理2]
|
||||
H1 --> Log[日志]
|
||||
H2 --> Log
|
||||
Process --> Resp[响应]
|
||||
\`\`\`
|
||||
|
||||
**图7-1 异常处理流程图**
|
||||
|
||||
### 7.1 异常类型
|
||||
| 异常类 | 错误码 | HTTP状态 | 说明 |
|
||||
|--------|--------|----------|------|
|
||||
|
||||
### 7.2 恢复策略
|
||||
| 场景 | 策略 | 说明 |
|
||||
|------|------|------|
|
||||
|
||||
[FOCUS]
|
||||
1. 异常类型: 自定义异常类及继承关系
|
||||
2. 错误码: 错误码定义和分类
|
||||
3. 处理模式: try-catch/中间件/装饰器
|
||||
4. 恢复策略: 重试/降级/熔断/告警
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-7-exceptions.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Output
|
||||
|
||||
各 Agent 写入 `sections/section-N-xxx.md`,返回简要 JSON 供 Phase 2.5 汇总。
|
||||
192
.claude/skills/copyright-docs/phases/02.5-consolidation.md
Normal file
192
.claude/skills/copyright-docs/phases/02.5-consolidation.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Phase 2.5: Consolidation Agent
|
||||
|
||||
汇总所有分析 Agent 的产出,生成设计综述,为 Phase 4 索引文档提供内容。
|
||||
|
||||
> **规范参考**: [../specs/cpcc-requirements.md](../specs/cpcc-requirements.md)
|
||||
|
||||
## 核心职责
|
||||
|
||||
1. **设计综述**:生成 synthesis(软件整体设计思路)
|
||||
2. **章节摘要**:生成 section_summaries(导航表格内容)
|
||||
3. **跨模块分析**:识别问题和关联
|
||||
4. **质量检查**:验证 CPCC 合规性
|
||||
|
||||
## 输入
|
||||
|
||||
```typescript
|
||||
interface ConsolidationInput {
|
||||
output_dir: string;
|
||||
agent_summaries: AgentReturn[];
|
||||
cross_module_notes: string[];
|
||||
metadata: ProjectMetadata;
|
||||
}
|
||||
```
|
||||
|
||||
## 执行
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
## 规范前置
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/cpcc-requirements.md
|
||||
严格遵循 CPCC 软著申请规范要求。
|
||||
|
||||
## 任务
|
||||
作为汇总 Agent,读取所有章节文件,生成设计综述和跨模块分析报告。
|
||||
|
||||
## 输入
|
||||
- 章节文件: ${outputDir}/sections/section-*.md
|
||||
- Agent 摘要: ${JSON.stringify(agent_summaries)}
|
||||
- 跨模块备注: ${JSON.stringify(cross_module_notes)}
|
||||
- 软件信息: ${JSON.stringify(metadata)}
|
||||
|
||||
## 核心产出
|
||||
|
||||
### 1. 设计综述 (synthesis)
|
||||
用 2-3 段落描述软件整体设计思路:
|
||||
- 第一段:软件定位与核心设计理念
|
||||
- 第二段:模块划分与协作机制
|
||||
- 第三段:技术选型与设计特点
|
||||
|
||||
### 2. 章节摘要 (section_summaries)
|
||||
为每个章节提取一句话说明,用于导航表格:
|
||||
|
||||
| 章节 | 文件 | 一句话说明 |
|
||||
|------|------|------------|
|
||||
| 2. 系统架构设计 | section-2-architecture.md | ... |
|
||||
| 3. 功能模块设计 | section-3-functions.md | ... |
|
||||
| 4. 核心算法与流程 | section-4-algorithms.md | ... |
|
||||
| 5. 数据结构设计 | section-5-data-structures.md | ... |
|
||||
| 6. 接口设计 | section-6-interfaces.md | ... |
|
||||
| 7. 异常处理设计 | section-7-exceptions.md | ... |
|
||||
|
||||
### 3. 跨模块分析
|
||||
- 一致性:术语、命名规范
|
||||
- 完整性:功能-接口对应、异常覆盖
|
||||
- 关联性:模块依赖、数据流向
|
||||
|
||||
## 输出文件
|
||||
|
||||
写入: ${outputDir}/cross-module-summary.md
|
||||
|
||||
### 文件格式
|
||||
|
||||
\`\`\`markdown
|
||||
# 跨模块分析报告
|
||||
|
||||
## 设计综述
|
||||
|
||||
[2-3 段落的软件设计思路描述]
|
||||
|
||||
## 章节摘要
|
||||
|
||||
| 章节 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| 2. 系统架构设计 | section-2-architecture.md | 一句话说明 |
|
||||
| ... | ... | ... |
|
||||
|
||||
## 文档统计
|
||||
|
||||
| 章节 | 图表数 | 字数 |
|
||||
|------|--------|------|
|
||||
| ... | ... | ... |
|
||||
|
||||
## 发现的问题
|
||||
|
||||
### 严重问题 (必须修复)
|
||||
|
||||
| ID | 类型 | 位置 | 描述 | 建议 |
|
||||
|----|------|------|------|------|
|
||||
| E001 | ... | ... | ... | ... |
|
||||
|
||||
### 警告 (建议修复)
|
||||
|
||||
| ID | 类型 | 位置 | 描述 | 建议 |
|
||||
|----|------|------|------|------|
|
||||
| W001 | ... | ... | ... | ... |
|
||||
|
||||
### 提示 (可选修复)
|
||||
|
||||
| ID | 类型 | 位置 | 描述 |
|
||||
|----|------|------|------|
|
||||
| I001 | ... | ... | ... |
|
||||
|
||||
## 跨模块关联图
|
||||
|
||||
\`\`\`mermaid
|
||||
graph LR
|
||||
S2[架构] --> S3[功能]
|
||||
S3 --> S4[算法]
|
||||
S3 --> S6[接口]
|
||||
S5[数据结构] --> S6
|
||||
S6 --> S7[异常]
|
||||
\`\`\`
|
||||
|
||||
## 修复建议优先级
|
||||
|
||||
[按优先级排序的建议,段落式描述]
|
||||
\`\`\`
|
||||
|
||||
## 返回格式 (JSON)
|
||||
|
||||
{
|
||||
"status": "completed",
|
||||
"output_file": "cross-module-summary.md",
|
||||
|
||||
// Phase 4 索引文档所需
|
||||
"synthesis": "2-3 段落的设计综述文本",
|
||||
"section_summaries": [
|
||||
{"file": "section-2-architecture.md", "title": "2. 系统架构设计", "summary": "一句话说明"},
|
||||
{"file": "section-3-functions.md", "title": "3. 功能模块设计", "summary": "一句话说明"},
|
||||
{"file": "section-4-algorithms.md", "title": "4. 核心算法与流程", "summary": "一句话说明"},
|
||||
{"file": "section-5-data-structures.md", "title": "5. 数据结构设计", "summary": "一句话说明"},
|
||||
{"file": "section-6-interfaces.md", "title": "6. 接口设计", "summary": "一句话说明"},
|
||||
{"file": "section-7-exceptions.md", "title": "7. 异常处理设计", "summary": "一句话说明"}
|
||||
],
|
||||
|
||||
// 质量信息
|
||||
"stats": {
|
||||
"total_sections": 6,
|
||||
"total_diagrams": 8,
|
||||
"total_words": 3500
|
||||
},
|
||||
"issues": {
|
||||
"errors": [...],
|
||||
"warnings": [...],
|
||||
"info": [...]
|
||||
},
|
||||
"cross_refs": {
|
||||
"found": 12,
|
||||
"missing": 3
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
## 问题分类
|
||||
|
||||
| 严重级别 | 前缀 | 含义 | 处理方式 |
|
||||
|----------|------|------|----------|
|
||||
| Error | E | 阻塞合规检查 | 必须修复 |
|
||||
| Warning | W | 影响文档质量 | 建议修复 |
|
||||
| Info | I | 可改进项 | 可选修复 |
|
||||
|
||||
## 问题类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| missing | 缺失内容(功能-接口对应、异常覆盖)|
|
||||
| inconsistency | 不一致(术语、命名、编号)|
|
||||
| circular | 循环依赖 |
|
||||
| orphan | 孤立内容(未被引用)|
|
||||
| syntax | Mermaid 语法错误 |
|
||||
| enhancement | 增强建议 |
|
||||
|
||||
## Output
|
||||
|
||||
- **文件**: `cross-module-summary.md`(完整汇总报告)
|
||||
- **返回**: JSON 包含 Phase 4 所需的 synthesis 和 section_summaries
|
||||
261
.claude/skills/copyright-docs/phases/04-document-assembly.md
Normal file
261
.claude/skills/copyright-docs/phases/04-document-assembly.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# Phase 4: Document Assembly
|
||||
|
||||
生成索引式文档,通过 markdown 链接引用章节文件。
|
||||
|
||||
> **规范参考**: [../specs/cpcc-requirements.md](../specs/cpcc-requirements.md)
|
||||
|
||||
## 设计原则
|
||||
|
||||
1. **引用而非嵌入**:主文档通过链接引用章节,不复制内容
|
||||
2. **索引 + 综述**:主文档提供导航和软件概述
|
||||
3. **CPCC 合规**:保持章节编号符合软著申请要求
|
||||
4. **独立可读**:各章节文件可单独阅读
|
||||
|
||||
## 输入
|
||||
|
||||
```typescript
|
||||
interface AssemblyInput {
|
||||
output_dir: string;
|
||||
metadata: ProjectMetadata;
|
||||
consolidation: {
|
||||
synthesis: string; // 跨章节综合分析
|
||||
section_summaries: Array<{
|
||||
file: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
}>;
|
||||
issues: { errors: Issue[], warnings: Issue[], info: Issue[] };
|
||||
stats: { total_sections: number, total_diagrams: number };
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
```javascript
|
||||
// 1. 检查是否有阻塞性问题
|
||||
if (consolidation.issues.errors.length > 0) {
|
||||
const response = await AskUserQuestion({
|
||||
questions: [{
|
||||
question: `发现 ${consolidation.issues.errors.length} 个严重问题,如何处理?`,
|
||||
header: "阻塞问题",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{label: "查看并修复", description: "显示问题列表,手动修复后重试"},
|
||||
{label: "忽略继续", description: "跳过问题检查,继续装配"},
|
||||
{label: "终止", description: "停止文档生成"}
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
if (response === "查看并修复") {
|
||||
return { action: "fix_required", errors: consolidation.issues.errors };
|
||||
}
|
||||
if (response === "终止") {
|
||||
return { action: "abort" };
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 生成索引式文档(不读取章节内容)
|
||||
const doc = generateIndexDocument(metadata, consolidation);
|
||||
|
||||
// 3. 写入最终文件
|
||||
Write(`${outputDir}/${metadata.software_name}-软件设计说明书.md`, doc);
|
||||
```
|
||||
|
||||
## 文档模板
|
||||
|
||||
```markdown
|
||||
<!-- 页眉:{软件名称} - 版本号:{版本号} -->
|
||||
|
||||
# {软件名称} 软件设计说明书
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 软件名称 | {software_name} |
|
||||
| 版本号 | {version} |
|
||||
| 生成日期 | {date} |
|
||||
|
||||
---
|
||||
|
||||
## 1. 软件概述
|
||||
|
||||
### 1.1 软件背景与用途
|
||||
|
||||
[从 metadata 生成的软件背景描述]
|
||||
|
||||
### 1.2 开发目标与特点
|
||||
|
||||
[从 metadata 生成的目标和特点]
|
||||
|
||||
### 1.3 运行环境与技术架构
|
||||
|
||||
[从 metadata.tech_stack 生成]
|
||||
|
||||
---
|
||||
|
||||
## 文档导航
|
||||
|
||||
{consolidation.synthesis - 软件整体设计思路综述}
|
||||
|
||||
| 章节 | 说明 | 详情 |
|
||||
|------|------|------|
|
||||
| 2. 系统架构设计 | {summary} | [查看](./sections/section-2-architecture.md) |
|
||||
| 3. 功能模块设计 | {summary} | [查看](./sections/section-3-functions.md) |
|
||||
| 4. 核心算法与流程 | {summary} | [查看](./sections/section-4-algorithms.md) |
|
||||
| 5. 数据结构设计 | {summary} | [查看](./sections/section-5-data-structures.md) |
|
||||
| 6. 接口设计 | {summary} | [查看](./sections/section-6-interfaces.md) |
|
||||
| 7. 异常处理设计 | {summary} | [查看](./sections/section-7-exceptions.md) |
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
- [跨模块分析报告](./cross-module-summary.md)
|
||||
- [章节文件目录](./sections/)
|
||||
|
||||
---
|
||||
|
||||
<!-- 页脚:生成时间 {timestamp} -->
|
||||
```
|
||||
|
||||
## 生成函数
|
||||
|
||||
```javascript
|
||||
function generateIndexDocument(metadata, consolidation) {
|
||||
const date = new Date().toLocaleDateString('zh-CN');
|
||||
|
||||
// 章节导航表格
|
||||
const sectionTable = consolidation.section_summaries
|
||||
.map(s => `| ${s.title} | ${s.summary} | [查看](./sections/${s.file}) |`)
|
||||
.join('\n');
|
||||
|
||||
return `<!-- 页眉:${metadata.software_name} - 版本号:${metadata.version} -->
|
||||
|
||||
# ${metadata.software_name} 软件设计说明书
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 软件名称 | ${metadata.software_name} |
|
||||
| 版本号 | ${metadata.version} |
|
||||
| 生成日期 | ${date} |
|
||||
|
||||
---
|
||||
|
||||
## 1. 软件概述
|
||||
|
||||
### 1.1 软件背景与用途
|
||||
|
||||
${generateBackground(metadata)}
|
||||
|
||||
### 1.2 开发目标与特点
|
||||
|
||||
${generateObjectives(metadata)}
|
||||
|
||||
### 1.3 运行环境与技术架构
|
||||
|
||||
${generateTechStack(metadata)}
|
||||
|
||||
---
|
||||
|
||||
## 设计综述
|
||||
|
||||
${consolidation.synthesis}
|
||||
|
||||
---
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 章节 | 说明 | 详情 |
|
||||
|------|------|------|
|
||||
${sectionTable}
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
- [跨模块分析报告](./cross-module-summary.md)
|
||||
- [章节文件目录](./sections/)
|
||||
|
||||
---
|
||||
|
||||
<!-- 页脚:生成时间 ${new Date().toISOString()} -->
|
||||
`;
|
||||
}
|
||||
|
||||
function generateBackground(metadata) {
|
||||
const categoryDescriptions = {
|
||||
"命令行工具 (CLI)": "提供命令行界面,用户通过终端命令与系统交互",
|
||||
"后端服务/API": "提供 RESTful/GraphQL API 接口,支持前端或其他服务调用",
|
||||
"SDK/库": "提供可复用的代码库,供其他项目集成使用",
|
||||
"数据处理系统": "处理数据导入、转换、分析和导出",
|
||||
"自动化脚本": "自动执行重复性任务,提高工作效率"
|
||||
};
|
||||
|
||||
return `${metadata.software_name}是一款${metadata.category}软件。${categoryDescriptions[metadata.category] || ''}
|
||||
|
||||
本软件基于${metadata.tech_stack.language}语言开发,运行于${metadata.tech_stack.runtime}环境,采用${metadata.tech_stack.framework || '原生'}框架实现核心功能。`;
|
||||
}
|
||||
|
||||
function generateObjectives(metadata) {
|
||||
return `本软件旨在${metadata.purpose || '解决特定领域的技术问题'}。
|
||||
|
||||
主要技术特点包括${metadata.tech_stack.framework ? `采用 ${metadata.tech_stack.framework} 框架` : '模块化设计'},具备良好的可扩展性和可维护性。`;
|
||||
}
|
||||
|
||||
function generateTechStack(metadata) {
|
||||
return `**运行环境**
|
||||
|
||||
- 操作系统:${metadata.os || 'Windows/Linux/macOS'}
|
||||
- 运行时:${metadata.tech_stack.runtime}
|
||||
- 依赖环境:${metadata.tech_stack.dependencies?.join(', ') || '无特殊依赖'}
|
||||
|
||||
**技术架构**
|
||||
|
||||
- 架构模式:${metadata.architecture_pattern || '分层架构'}
|
||||
- 核心框架:${metadata.tech_stack.framework || '原生实现'}
|
||||
- 主要模块:详见第2章系统架构设计`;
|
||||
}
|
||||
```
|
||||
|
||||
## 输出结构
|
||||
|
||||
```
|
||||
.workflow/.scratchpad/copyright-{timestamp}/
|
||||
├── sections/ # 独立章节(Phase 2 产出)
|
||||
│ ├── section-2-architecture.md
|
||||
│ ├── section-3-functions.md
|
||||
│ └── ...
|
||||
├── cross-module-summary.md # 跨模块报告(Phase 2.5 产出)
|
||||
└── {软件名称}-软件设计说明书.md # 索引文档(本阶段产出)
|
||||
```
|
||||
|
||||
## 与 Phase 2.5 的协作
|
||||
|
||||
Phase 2.5 consolidation agent 需要提供:
|
||||
|
||||
```typescript
|
||||
interface ConsolidationOutput {
|
||||
synthesis: string; // 设计思路综述(2-3 段落)
|
||||
section_summaries: Array<{
|
||||
file: string; // 文件名
|
||||
title: string; // 章节标题(如"2. 系统架构设计")
|
||||
summary: string; // 一句话说明
|
||||
}>;
|
||||
issues: {...};
|
||||
stats: {...};
|
||||
}
|
||||
```
|
||||
|
||||
## 关键变更
|
||||
|
||||
| 原设计 | 新设计 |
|
||||
|--------|--------|
|
||||
| 读取章节内容并拼接 | 链接引用,不读取内容 |
|
||||
| 嵌入完整章节 | 仅提供导航索引 |
|
||||
| 重复生成统计 | 引用 cross-module-summary.md |
|
||||
| 大文件 | 精简索引文档 |
|
||||
192
.claude/skills/copyright-docs/phases/05-compliance-refinement.md
Normal file
192
.claude/skills/copyright-docs/phases/05-compliance-refinement.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Phase 5: Compliance Review & Iterative Refinement
|
||||
|
||||
Discovery-driven refinement loop until CPCC compliance is met.
|
||||
|
||||
## Execution
|
||||
|
||||
### Step 1: Extract Compliance Issues
|
||||
|
||||
```javascript
|
||||
function extractComplianceIssues(validationResult, deepAnalysis) {
|
||||
return {
|
||||
// Missing or incomplete sections
|
||||
missingSections: validationResult.details
|
||||
.filter(d => !d.pass)
|
||||
.map(d => ({
|
||||
section: d.name,
|
||||
severity: 'critical',
|
||||
suggestion: `需要补充 ${d.name} 相关内容`
|
||||
})),
|
||||
|
||||
// Features with weak descriptions (< 50 chars)
|
||||
weakDescriptions: (deepAnalysis.functions?.feature_list || [])
|
||||
.filter(f => !f.description || f.description.length < 50)
|
||||
.map(f => ({
|
||||
feature: f.name,
|
||||
current: f.description || '(无描述)',
|
||||
severity: 'warning'
|
||||
})),
|
||||
|
||||
// Complex algorithms without detailed flowcharts
|
||||
complexAlgorithms: (deepAnalysis.algorithms?.algorithms || [])
|
||||
.filter(a => (a.complexity || 0) > 10 && (a.steps?.length || 0) < 5)
|
||||
.map(a => ({
|
||||
algorithm: a.name,
|
||||
complexity: a.complexity,
|
||||
file: a.file,
|
||||
severity: 'warning'
|
||||
})),
|
||||
|
||||
// Data relationships without descriptions
|
||||
incompleteRelationships: (deepAnalysis.data_structures?.relationships || [])
|
||||
.filter(r => !r.description)
|
||||
.map(r => ({from: r.from, to: r.to, severity: 'info'})),
|
||||
|
||||
// Diagram validation issues
|
||||
diagramIssues: (deepAnalysis.diagrams?.validation || [])
|
||||
.filter(d => !d.valid)
|
||||
.map(d => ({file: d.file, issues: d.issues, severity: 'critical'}))
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Build Dynamic Questions
|
||||
|
||||
```javascript
|
||||
function buildComplianceQuestions(issues) {
|
||||
const questions = [];
|
||||
|
||||
if (issues.missingSections.length > 0) {
|
||||
questions.push({
|
||||
question: `发现 ${issues.missingSections.length} 个章节内容不完整,需要补充哪些?`,
|
||||
header: "章节补充",
|
||||
multiSelect: true,
|
||||
options: issues.missingSections.slice(0, 4).map(s => ({
|
||||
label: s.section,
|
||||
description: s.suggestion
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
if (issues.weakDescriptions.length > 0) {
|
||||
questions.push({
|
||||
question: `以下 ${issues.weakDescriptions.length} 个功能描述过于简短,请选择需要详细说明的:`,
|
||||
header: "功能描述",
|
||||
multiSelect: true,
|
||||
options: issues.weakDescriptions.slice(0, 4).map(f => ({
|
||||
label: f.feature,
|
||||
description: `当前:${f.current.substring(0, 30)}...`
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
if (issues.complexAlgorithms.length > 0) {
|
||||
questions.push({
|
||||
question: `发现 ${issues.complexAlgorithms.length} 个复杂算法缺少详细流程图,是否生成?`,
|
||||
header: "算法详解",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{label: "全部生成 (推荐)", description: "为所有复杂算法生成含分支/循环的流程图"},
|
||||
{label: "仅最复杂的", description: `仅为 ${issues.complexAlgorithms[0]?.algorithm} 生成`},
|
||||
{label: "跳过", description: "保持当前简单流程图"}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
questions.push({
|
||||
question: "如何处理当前文档?",
|
||||
header: "操作",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{label: "应用修改并继续", description: "应用上述选择,继续检查"},
|
||||
{label: "完成文档", description: "当前文档满足要求,生成最终版本"},
|
||||
{label: "重新分析", description: "使用不同配置重新分析代码"}
|
||||
]
|
||||
});
|
||||
|
||||
return questions.slice(0, 4);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Apply Updates
|
||||
|
||||
```javascript
|
||||
async function applyComplianceUpdates(responses, issues, analyses, outputDir) {
|
||||
const updates = [];
|
||||
|
||||
if (responses['章节补充']) {
|
||||
for (const section of responses['章节补充']) {
|
||||
const sectionAnalysis = await Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
prompt: `深入分析 ${section.section} 所需内容...`
|
||||
});
|
||||
updates.push({type: 'section_supplement', section: section.section, data: sectionAnalysis});
|
||||
}
|
||||
}
|
||||
|
||||
if (responses['算法详解'] === '全部生成 (推荐)') {
|
||||
for (const algo of issues.complexAlgorithms) {
|
||||
const detailedSteps = await analyzeAlgorithmInDepth(algo, analyses);
|
||||
const flowchart = generateAlgorithmFlowchart({
|
||||
name: algo.algorithm,
|
||||
inputs: detailedSteps.inputs,
|
||||
outputs: detailedSteps.outputs,
|
||||
steps: detailedSteps.steps
|
||||
});
|
||||
Write(`${outputDir}/diagrams/algorithm-${sanitizeId(algo.algorithm)}-detailed.mmd`, flowchart);
|
||||
updates.push({type: 'algorithm_flowchart', algorithm: algo.algorithm});
|
||||
}
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Iteration Loop
|
||||
|
||||
```javascript
|
||||
async function runComplianceLoop(documentPath, analyses, metadata, outputDir) {
|
||||
let iteration = 0;
|
||||
const maxIterations = 5;
|
||||
|
||||
while (iteration < maxIterations) {
|
||||
iteration++;
|
||||
|
||||
// Validate current document
|
||||
const document = Read(documentPath);
|
||||
const validation = validateCPCCCompliance(document, analyses);
|
||||
|
||||
// Extract issues
|
||||
const issues = extractComplianceIssues(validation, analyses);
|
||||
const totalIssues = Object.values(issues).flat().length;
|
||||
|
||||
if (totalIssues === 0) {
|
||||
console.log("✅ 所有检查通过,文档符合 CPCC 要求");
|
||||
break;
|
||||
}
|
||||
|
||||
// Ask user
|
||||
const questions = buildComplianceQuestions(issues);
|
||||
const responses = await AskUserQuestion({questions});
|
||||
|
||||
if (responses['操作'] === '完成文档') break;
|
||||
if (responses['操作'] === '重新分析') return {action: 'restart'};
|
||||
|
||||
// Apply updates
|
||||
const updates = await applyComplianceUpdates(responses, issues, analyses, outputDir);
|
||||
|
||||
// Regenerate document
|
||||
const updatedDocument = regenerateDocument(document, updates, analyses);
|
||||
Write(documentPath, updatedDocument);
|
||||
|
||||
// Archive iteration
|
||||
Write(`${outputDir}/iterations/v${iteration}.md`, document);
|
||||
}
|
||||
|
||||
return {action: 'finalized', iterations: iteration};
|
||||
}
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Final compliant document + iteration history in `iterations/`.
|
||||
121
.claude/skills/copyright-docs/specs/cpcc-requirements.md
Normal file
121
.claude/skills/copyright-docs/specs/cpcc-requirements.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# CPCC Compliance Requirements
|
||||
|
||||
China Copyright Protection Center (CPCC) requirements for software design specification.
|
||||
|
||||
## When to Use
|
||||
|
||||
| Phase | Usage | Section |
|
||||
|-------|-------|---------|
|
||||
| Phase 4 | Check document structure before assembly | Document Requirements, Mandatory Sections |
|
||||
| Phase 4 | Apply correct figure numbering | Figure Numbering Convention |
|
||||
| Phase 5 | Validate before each iteration | Validation Function |
|
||||
| Phase 5 | Handle failures during refinement | Error Handling |
|
||||
|
||||
---
|
||||
|
||||
## Document Requirements
|
||||
|
||||
### Format
|
||||
- [ ] 页眉包含软件名称和版本号
|
||||
- [ ] 页码位于右上角说明
|
||||
- [ ] 每页不少于30行文字(图表页除外)
|
||||
- [ ] A4纵向排版,文字从左至右
|
||||
|
||||
### Mandatory Sections (7 章节)
|
||||
- [ ] 1. 软件概述
|
||||
- [ ] 2. 系统架构图
|
||||
- [ ] 3. 功能模块设计
|
||||
- [ ] 4. 核心算法与流程
|
||||
- [ ] 5. 数据结构设计
|
||||
- [ ] 6. 接口设计
|
||||
- [ ] 7. 异常处理设计
|
||||
|
||||
### Content Requirements
|
||||
- [ ] 所有内容基于代码分析
|
||||
- [ ] 无臆测或未来计划
|
||||
- [ ] 无原始指令性文字
|
||||
- [ ] Mermaid 语法正确
|
||||
- [ ] 图表编号和说明完整
|
||||
|
||||
## Validation Function
|
||||
|
||||
```javascript
|
||||
function validateCPCCCompliance(document, analyses) {
|
||||
const checks = [
|
||||
{name: "软件概述完整性", pass: document.includes("## 1. 软件概述")},
|
||||
{name: "系统架构图存在", pass: document.includes("图2-1 系统架构图")},
|
||||
{name: "功能模块设计完整", pass: document.includes("## 3. 功能模块设计")},
|
||||
{name: "核心算法描述", pass: document.includes("## 4. 核心算法与流程")},
|
||||
{name: "数据结构设计", pass: document.includes("## 5. 数据结构设计")},
|
||||
{name: "接口设计说明", pass: document.includes("## 6. 接口设计")},
|
||||
{name: "异常处理设计", pass: document.includes("## 7. 异常处理设计")},
|
||||
{name: "Mermaid图表语法", pass: !document.includes("mermaid error")},
|
||||
{name: "页眉信息", pass: document.includes("页眉")},
|
||||
{name: "页码说明", pass: document.includes("页码")}
|
||||
];
|
||||
|
||||
return {
|
||||
passed: checks.filter(c => c.pass).length,
|
||||
total: checks.length,
|
||||
details: checks
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Software Categories
|
||||
|
||||
| Category | Document Focus |
|
||||
|----------|----------------|
|
||||
| 命令行工具 (CLI) | 命令、参数、使用流程 |
|
||||
| 后端服务/API | 端点、协议、数据流 |
|
||||
| SDK/库 | 接口、集成、使用示例 |
|
||||
| 数据处理系统 | 数据流、转换、ETL |
|
||||
| 自动化脚本 | 工作流、触发器、调度 |
|
||||
|
||||
## Figure Numbering Convention
|
||||
|
||||
| Section | Figure | Title |
|
||||
|---------|--------|-------|
|
||||
| 2 | 图2-1 | 系统架构图 |
|
||||
| 3 | 图3-1 | 功能模块结构图 |
|
||||
| 4 | 图4-N | {算法名称}流程图 |
|
||||
| 5 | 图5-1 | 数据结构类图 |
|
||||
| 6 | 图6-N | {接口名称}时序图 |
|
||||
| 7 | 图7-1 | 异常处理流程图 |
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Recovery |
|
||||
|-------|----------|
|
||||
| Analysis timeout | Reduce scope, retry |
|
||||
| Missing section data | Re-run targeted agent |
|
||||
| Diagram validation fails | Regenerate with fixes |
|
||||
| User abandons iteration | Save progress, allow resume |
|
||||
|
||||
---
|
||||
|
||||
## Integration with Phases
|
||||
|
||||
**Phase 4 - Document Assembly**:
|
||||
```javascript
|
||||
// Before assembling document
|
||||
const docChecks = [
|
||||
{check: "页眉格式", value: `<!-- 页眉:${metadata.software_name} - 版本号:${metadata.version} -->`},
|
||||
{check: "页码说明", value: `<!-- 注:最终文档页码位于每页右上角 -->`}
|
||||
];
|
||||
|
||||
// Apply figure numbering from convention table
|
||||
const figureNumbers = getFigureNumbers(sectionIndex);
|
||||
```
|
||||
|
||||
**Phase 5 - Compliance Refinement**:
|
||||
```javascript
|
||||
// In 05-compliance-refinement.md
|
||||
const validation = validateCPCCCompliance(document, analyses);
|
||||
|
||||
if (validation.passed < validation.total) {
|
||||
// Failed checks become discovery questions
|
||||
const failedChecks = validation.details.filter(d => !d.pass);
|
||||
discoveries.complianceIssues = failedChecks;
|
||||
}
|
||||
```
|
||||
200
.claude/skills/copyright-docs/templates/agent-base.md
Normal file
200
.claude/skills/copyright-docs/templates/agent-base.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Agent Base Template
|
||||
|
||||
所有分析 Agent 的基础模板,确保一致性和高效执行。
|
||||
|
||||
## 通用提示词结构
|
||||
|
||||
```
|
||||
[ROLE] 你是{角色},专注于{职责}。
|
||||
|
||||
[TASK]
|
||||
分析代码库,生成 CPCC 合规的章节文档。
|
||||
- 输出: {output_dir}/sections/{filename}
|
||||
- 格式: Markdown + Mermaid
|
||||
- 范围: {scope_path}
|
||||
|
||||
[CONSTRAINTS]
|
||||
- 只描述已实现的代码,不臆测
|
||||
- 中文输出,技术术语可用英文
|
||||
- Mermaid 图表必须可渲染
|
||||
- 文件/类/函数需包含路径引用
|
||||
|
||||
[OUTPUT_FORMAT]
|
||||
1. 直接写入 MD 文件
|
||||
2. 返回 JSON 简要信息
|
||||
|
||||
[QUALITY_CHECKLIST]
|
||||
- [ ] 包含至少1个 Mermaid 图表
|
||||
- [ ] 每个子章节有实质内容 (>100字)
|
||||
- [ ] 代码引用格式: `src/path/file.ts:line`
|
||||
- [ ] 图表编号正确 (图N-M)
|
||||
```
|
||||
|
||||
## 变量说明
|
||||
|
||||
| 变量 | 来源 | 示例 |
|
||||
|------|------|------|
|
||||
| {output_dir} | Phase 1 创建 | .workflow/.scratchpad/copyright-xxx |
|
||||
| {software_name} | metadata.software_name | 智能数据分析系统 |
|
||||
| {scope_path} | metadata.scope_path | src/ |
|
||||
| {tech_stack} | metadata.tech_stack | TypeScript/Node.js |
|
||||
|
||||
## Agent 提示词模板
|
||||
|
||||
### 精简版 (推荐)
|
||||
|
||||
```javascript
|
||||
const agentPrompt = (agent, meta, outDir) => `
|
||||
[ROLE] ${AGENT_ROLES[agent]}
|
||||
|
||||
[TASK]
|
||||
分析 ${meta.scope_path},生成 ${AGENT_SECTIONS[agent]}。
|
||||
输出: ${outDir}/sections/${AGENT_FILES[agent]}
|
||||
|
||||
[TEMPLATE]
|
||||
${AGENT_TEMPLATES[agent]}
|
||||
|
||||
[FOCUS]
|
||||
${AGENT_FOCUS[agent].join('\n')}
|
||||
|
||||
[RETURN]
|
||||
{"status":"completed","output_file":"${AGENT_FILES[agent]}","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`;
|
||||
```
|
||||
|
||||
### 配置映射
|
||||
|
||||
```javascript
|
||||
const AGENT_ROLES = {
|
||||
architecture: "系统架构师,专注于分层设计和模块依赖",
|
||||
functions: "功能分析师,专注于功能点识别和交互",
|
||||
algorithms: "算法工程师,专注于核心逻辑和复杂度",
|
||||
data_structures: "数据建模师,专注于实体关系和类型",
|
||||
interfaces: "API设计师,专注于接口契约和协议",
|
||||
exceptions: "可靠性工程师,专注于异常处理和恢复"
|
||||
};
|
||||
|
||||
const AGENT_SECTIONS = {
|
||||
architecture: "Section 2: 系统架构图",
|
||||
functions: "Section 3: 功能模块设计",
|
||||
algorithms: "Section 4: 核心算法与流程",
|
||||
data_structures: "Section 5: 数据结构设计",
|
||||
interfaces: "Section 6: 接口设计",
|
||||
exceptions: "Section 7: 异常处理设计"
|
||||
};
|
||||
|
||||
const AGENT_FILES = {
|
||||
architecture: "section-2-architecture.md",
|
||||
functions: "section-3-functions.md",
|
||||
algorithms: "section-4-algorithms.md",
|
||||
data_structures: "section-5-data-structures.md",
|
||||
interfaces: "section-6-interfaces.md",
|
||||
exceptions: "section-7-exceptions.md"
|
||||
};
|
||||
|
||||
const AGENT_FOCUS = {
|
||||
architecture: [
|
||||
"1. 分层: 识别代码层次 (Controller/Service/Repository)",
|
||||
"2. 模块: 核心模块及职责边界",
|
||||
"3. 依赖: 模块间依赖方向",
|
||||
"4. 数据流: 请求/数据的流动路径"
|
||||
],
|
||||
functions: [
|
||||
"1. 功能点: 枚举所有用户可见功能",
|
||||
"2. 模块分组: 按业务域分组",
|
||||
"3. 入口: 每个功能的代码入口",
|
||||
"4. 交互: 功能间的调用关系"
|
||||
],
|
||||
algorithms: [
|
||||
"1. 核心算法: 业务逻辑的关键算法",
|
||||
"2. 流程步骤: 分支/循环/条件",
|
||||
"3. 复杂度: 时间/空间复杂度",
|
||||
"4. 输入输出: 参数和返回值"
|
||||
],
|
||||
data_structures: [
|
||||
"1. 实体: class/interface/type 定义",
|
||||
"2. 属性: 字段类型和可见性",
|
||||
"3. 关系: 继承/组合/关联",
|
||||
"4. 枚举: 枚举类型及其值"
|
||||
],
|
||||
interfaces: [
|
||||
"1. API端点: 路径/方法/说明",
|
||||
"2. 参数: 请求参数类型和校验",
|
||||
"3. 响应: 响应格式和状态码",
|
||||
"4. 时序: 典型调用流程"
|
||||
],
|
||||
exceptions: [
|
||||
"1. 异常类型: 自定义异常类",
|
||||
"2. 错误码: 错误码定义和含义",
|
||||
"3. 处理模式: try-catch/中间件",
|
||||
"4. 恢复策略: 重试/降级/告警"
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
## 效率优化
|
||||
|
||||
### 1. 减少冗余
|
||||
|
||||
**Before (冗余)**:
|
||||
```
|
||||
你是一个专业的系统架构师,具有丰富的软件设计经验。
|
||||
你需要分析代码库,识别系统的分层结构...
|
||||
```
|
||||
|
||||
**After (精简)**:
|
||||
```
|
||||
[ROLE] 系统架构师,专注于分层设计和模块依赖。
|
||||
[TASK] 分析 src/,生成系统架构图章节。
|
||||
```
|
||||
|
||||
### 2. 模板驱动
|
||||
|
||||
**Before (描述性)**:
|
||||
```
|
||||
请按照以下格式输出:
|
||||
首先写一个二级标题...
|
||||
然后添加一个Mermaid图...
|
||||
```
|
||||
|
||||
**After (模板)**:
|
||||
```
|
||||
[TEMPLATE]
|
||||
## 2. 系统架构图
|
||||
{intro}
|
||||
\`\`\`mermaid
|
||||
{diagram}
|
||||
\`\`\`
|
||||
**图2-1 系统架构图**
|
||||
### 2.1 {subsection}
|
||||
{content}
|
||||
```
|
||||
|
||||
### 3. 焦点明确
|
||||
|
||||
**Before (模糊)**:
|
||||
```
|
||||
分析项目的各个方面,包括架构、模块、依赖等
|
||||
```
|
||||
|
||||
**After (具体)**:
|
||||
```
|
||||
[FOCUS]
|
||||
1. 分层: Controller/Service/Repository
|
||||
2. 模块: 职责边界
|
||||
3. 依赖: 方向性
|
||||
4. 数据流: 路径
|
||||
```
|
||||
|
||||
### 4. 返回简洁
|
||||
|
||||
**Before (冗长)**:
|
||||
```
|
||||
请返回详细的分析结果,包括所有发现的问题...
|
||||
```
|
||||
|
||||
**After (结构化)**:
|
||||
```
|
||||
[RETURN]
|
||||
{"status":"completed","output_file":"xxx.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
```
|
||||
162
.claude/skills/project-analyze/SKILL.md
Normal file
162
.claude/skills/project-analyze/SKILL.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
name: project-analyze
|
||||
description: Multi-phase iterative project analysis with Mermaid diagrams. Generates architecture reports, design reports, method analysis reports. Use when analyzing codebases, understanding project structure, reviewing architecture, exploring design patterns, or documenting system components. Triggers on "analyze project", "architecture report", "design analysis", "code structure", "system overview".
|
||||
allowed-tools: Task, AskUserQuestion, Read, Bash, Glob, Grep, Write
|
||||
---
|
||||
|
||||
# Project Analysis Skill
|
||||
|
||||
Generate comprehensive project analysis reports through multi-phase iterative workflow.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Context-Optimized Architecture │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Phase 1: Requirements → analysis-config.json │
|
||||
│ ↓ │
|
||||
│ Phase 2: Exploration → 初步探索,确定范围 │
|
||||
│ ↓ │
|
||||
│ Phase 3: Parallel Agents → sections/section-*.md (直接写MD) │
|
||||
│ ↓ 返回简要JSON │
|
||||
│ Phase 3.5: Consolidation → consolidation-summary.md │
|
||||
│ Agent ↓ 返回质量评分+问题列表 │
|
||||
│ ↓ │
|
||||
│ Phase 4: Assembly → 合并MD + 质量附录 │
|
||||
│ ↓ │
|
||||
│ Phase 5: Refinement → 最终报告 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
1. **Agent 直接输出 MD**: 避免 JSON → MD 转换的上下文开销
|
||||
2. **简要返回**: Agent 只返回路径+摘要,不返回完整内容
|
||||
3. **汇总 Agent**: 独立 Agent 负责跨章节问题检测和质量评分
|
||||
4. **引用合并**: Phase 4 读取文件合并,不在上下文中传递
|
||||
5. **段落式描述**: 禁止清单罗列,层层递进,客观学术表达
|
||||
|
||||
## Execution Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Phase 1: Requirements Discovery │
|
||||
│ → Read: phases/01-requirements-discovery.md │
|
||||
│ → Collect: report type, depth level, scope, focus areas │
|
||||
│ → Output: analysis-config.json │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 2: Project Exploration │
|
||||
│ → Read: phases/02-project-exploration.md │
|
||||
│ → Launch: parallel exploration agents │
|
||||
│ → Output: exploration context for Phase 3 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 3: Deep Analysis (Parallel Agents) │
|
||||
│ → Read: phases/03-deep-analysis.md │
|
||||
│ → Reference: specs/quality-standards.md │
|
||||
│ → Each Agent: 分析代码 → 直接写 sections/section-*.md │
|
||||
│ → Return: {"status", "output_file", "summary", "cross_notes"} │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 3.5: Consolidation (New!) │
|
||||
│ → Read: phases/03.5-consolidation.md │
|
||||
│ → Input: Agent 返回的简要信息 + cross_module_notes │
|
||||
│ → Analyze: 一致性/完整性/关联性/质量检查 │
|
||||
│ → Output: consolidation-summary.md │
|
||||
│ → Return: {"quality_score", "issues", "stats"} │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 4: Report Generation │
|
||||
│ → Read: phases/04-report-generation.md │
|
||||
│ → Check: 如有 errors,提示用户处理 │
|
||||
│ → Merge: Executive Summary + sections/*.md + 质量附录 │
|
||||
│ → Output: {TYPE}-REPORT.md │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 5: Iterative Refinement │
|
||||
│ → Read: phases/05-iterative-refinement.md │
|
||||
│ → Reference: specs/quality-standards.md │
|
||||
│ → Loop: 发现问题 → 提问 → 修复 → 重新检查 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Report Types
|
||||
|
||||
| Type | Output | Agents | Focus |
|
||||
|------|--------|--------|-------|
|
||||
| `architecture` | ARCHITECTURE-REPORT.md | 5 | System structure, modules, dependencies |
|
||||
| `design` | DESIGN-REPORT.md | 4 | Patterns, classes, interfaces |
|
||||
| `methods` | METHODS-REPORT.md | 4 | Algorithms, critical paths, APIs |
|
||||
| `comprehensive` | COMPREHENSIVE-REPORT.md | All | All above combined |
|
||||
|
||||
## Agent Configuration by Report Type
|
||||
|
||||
### Architecture Report
|
||||
| Agent | Output File | Section |
|
||||
|-------|-------------|---------|
|
||||
| overview | section-overview.md | System Overview |
|
||||
| layers | section-layers.md | Layer Analysis |
|
||||
| dependencies | section-dependencies.md | Module Dependencies |
|
||||
| dataflow | section-dataflow.md | Data Flow |
|
||||
| entrypoints | section-entrypoints.md | Entry Points |
|
||||
|
||||
### Design Report
|
||||
| Agent | Output File | Section |
|
||||
|-------|-------------|---------|
|
||||
| patterns | section-patterns.md | Design Patterns |
|
||||
| classes | section-classes.md | Class Relationships |
|
||||
| interfaces | section-interfaces.md | Interface Contracts |
|
||||
| state | section-state.md | State Management |
|
||||
|
||||
### Methods Report
|
||||
| Agent | Output File | Section |
|
||||
|-------|-------------|---------|
|
||||
| algorithms | section-algorithms.md | Core Algorithms |
|
||||
| paths | section-paths.md | Critical Code Paths |
|
||||
| apis | section-apis.md | Public API Reference |
|
||||
| logic | section-logic.md | Complex Logic |
|
||||
|
||||
## Directory Setup
|
||||
|
||||
```javascript
|
||||
// 生成时间戳目录名
|
||||
const timestamp = new Date().toISOString().slice(0,19).replace(/[-:T]/g, '');
|
||||
const dir = `.workflow/.scratchpad/analyze-${timestamp}`;
|
||||
|
||||
// Windows (cmd)
|
||||
Bash(`mkdir "${dir}\\sections"`);
|
||||
Bash(`mkdir "${dir}\\iterations"`);
|
||||
|
||||
// Unix/macOS
|
||||
// Bash(`mkdir -p "${dir}/sections" "${dir}/iterations"`);
|
||||
```
|
||||
|
||||
## Output Structure
|
||||
|
||||
```
|
||||
.workflow/.scratchpad/analyze-{timestamp}/
|
||||
├── analysis-config.json # Phase 1
|
||||
├── sections/ # Phase 3 (Agent 直接写入)
|
||||
│ ├── section-overview.md
|
||||
│ ├── section-layers.md
|
||||
│ ├── section-dependencies.md
|
||||
│ └── ...
|
||||
├── consolidation-summary.md # Phase 3.5
|
||||
├── {TYPE}-REPORT.md # Final Output
|
||||
└── iterations/ # Phase 5
|
||||
├── v1.md
|
||||
└── v2.md
|
||||
```
|
||||
|
||||
## Reference Documents
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [phases/01-requirements-discovery.md](phases/01-requirements-discovery.md) | User interaction, config collection |
|
||||
| [phases/02-project-exploration.md](phases/02-project-exploration.md) | Initial exploration |
|
||||
| [phases/03-deep-analysis.md](phases/03-deep-analysis.md) | Parallel agent analysis |
|
||||
| [phases/03.5-consolidation.md](phases/03.5-consolidation.md) | Cross-section consolidation |
|
||||
| [phases/04-report-generation.md](phases/04-report-generation.md) | Report assembly |
|
||||
| [phases/05-iterative-refinement.md](phases/05-iterative-refinement.md) | Quality refinement |
|
||||
| [specs/quality-standards.md](specs/quality-standards.md) | Quality gates, standards |
|
||||
| [specs/writing-style.md](specs/writing-style.md) | 段落式学术写作规范 |
|
||||
| [../_shared/mermaid-utils.md](../_shared/mermaid-utils.md) | Shared Mermaid utilities |
|
||||
@@ -0,0 +1,79 @@
|
||||
# Phase 1: Requirements Discovery
|
||||
|
||||
Collect user requirements before analysis begins.
|
||||
|
||||
## Execution
|
||||
|
||||
### Step 1: Report Type Selection
|
||||
|
||||
```javascript
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: "What type of project analysis report would you like?",
|
||||
header: "Report Type",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{label: "Architecture (Recommended)", description: "System structure, module relationships, layer analysis, dependency graph"},
|
||||
{label: "Design", description: "Design patterns, class relationships, component interactions, abstraction analysis"},
|
||||
{label: "Methods", description: "Key algorithms, critical code paths, core function explanations with examples"},
|
||||
{label: "Comprehensive", description: "All above combined into a complete project analysis"}
|
||||
]
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
### Step 2: Depth Level Selection
|
||||
|
||||
```javascript
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: "What depth level do you need?",
|
||||
header: "Depth",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{label: "Overview", description: "High-level understanding, suitable for onboarding"},
|
||||
{label: "Detailed", description: "In-depth analysis with code examples"},
|
||||
{label: "Deep-Dive", description: "Exhaustive analysis with implementation details"}
|
||||
]
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
### Step 3: Scope Definition
|
||||
|
||||
```javascript
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: "What scope should the analysis cover?",
|
||||
header: "Scope",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{label: "Full Project", description: "Analyze entire codebase"},
|
||||
{label: "Specific Module", description: "Focus on a specific module or directory"},
|
||||
{label: "Custom Path", description: "Specify custom path pattern"}
|
||||
]
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
## Focus Areas Mapping
|
||||
|
||||
| Report Type | Focus Areas |
|
||||
|-------------|-------------|
|
||||
| Architecture | Layer Structure, Module Dependencies, Entry Points, Data Flow |
|
||||
| Design | Design Patterns, Class Relationships, Interface Contracts, State Management |
|
||||
| Methods | Core Algorithms, Critical Paths, Public APIs, Complex Logic |
|
||||
| Comprehensive | All above combined |
|
||||
|
||||
## Output
|
||||
|
||||
Save configuration to `analysis-config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "architecture|design|methods|comprehensive",
|
||||
"depth": "overview|detailed|deep-dive",
|
||||
"scope": "**/*|src/**/*|custom",
|
||||
"focus_areas": ["..."]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
# Phase 2: Project Exploration
|
||||
|
||||
Launch parallel exploration agents based on report type.
|
||||
|
||||
## Execution
|
||||
|
||||
### Step 1: Map Exploration Angles
|
||||
|
||||
```javascript
|
||||
const angleMapping = {
|
||||
architecture: ["Layer Structure", "Module Dependencies", "Entry Points", "Data Flow"],
|
||||
design: ["Design Patterns", "Class Relationships", "Interface Contracts", "State Management"],
|
||||
methods: ["Core Algorithms", "Critical Paths", "Public APIs", "Complex Logic"],
|
||||
comprehensive: ["Layer Structure", "Design Patterns", "Core Algorithms", "Data Flow"]
|
||||
};
|
||||
|
||||
const angles = angleMapping[config.type];
|
||||
```
|
||||
|
||||
### Step 2: Launch Parallel Agents
|
||||
|
||||
For each angle, launch an exploration agent:
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
description: `Explore: ${angle}`,
|
||||
prompt: `
|
||||
## Exploration Objective
|
||||
Execute **${angle}** exploration for project analysis report.
|
||||
|
||||
## Context
|
||||
- **Angle**: ${angle}
|
||||
- **Report Type**: ${config.type}
|
||||
- **Depth**: ${config.depth}
|
||||
- **Scope**: ${config.scope}
|
||||
|
||||
## Exploration Protocol
|
||||
1. Structural Discovery (get_modules_by_depth, rg, glob)
|
||||
2. Pattern Recognition (conventions, naming, organization)
|
||||
3. Relationship Mapping (dependencies, integration points)
|
||||
|
||||
## Output Format
|
||||
{
|
||||
"angle": "${angle}",
|
||||
"findings": {
|
||||
"structure": [...],
|
||||
"patterns": [...],
|
||||
"relationships": [...],
|
||||
"key_files": [{path, relevance, rationale}]
|
||||
},
|
||||
"insights": [...]
|
||||
}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Step 3: Aggregate Results
|
||||
|
||||
Merge all exploration results into unified findings:
|
||||
|
||||
```javascript
|
||||
const aggregatedFindings = {
|
||||
structure: [], // from all angles
|
||||
patterns: [], // from all angles
|
||||
relationships: [], // from all angles
|
||||
key_files: [], // deduplicated
|
||||
insights: [] // prioritized
|
||||
};
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Save exploration results to `exploration-{angle}.json` files.
|
||||
640
.claude/skills/project-analyze/phases/03-deep-analysis.md
Normal file
640
.claude/skills/project-analyze/phases/03-deep-analysis.md
Normal file
@@ -0,0 +1,640 @@
|
||||
# Phase 3: Deep Analysis
|
||||
|
||||
并行 Agent 撰写设计报告章节,返回简要信息。
|
||||
|
||||
> **规范参考**: [../specs/quality-standards.md](../specs/quality-standards.md)
|
||||
> **写作风格**: [../specs/writing-style.md](../specs/writing-style.md)
|
||||
|
||||
## Agent 执行前置条件
|
||||
|
||||
**每个 Agent 必须首先读取以下规范文件**:
|
||||
|
||||
```javascript
|
||||
// Agent 启动时的第一步操作
|
||||
const specs = {
|
||||
quality: Read(`${skillRoot}/specs/quality-standards.md`),
|
||||
style: Read(`${skillRoot}/specs/writing-style.md`)
|
||||
};
|
||||
```
|
||||
|
||||
规范文件路径(相对于 skill 根目录):
|
||||
- `specs/quality-standards.md` - 质量标准和检查清单
|
||||
- `specs/writing-style.md` - 段落式写作规范
|
||||
|
||||
---
|
||||
|
||||
## 通用写作规范(所有 Agent 共用)
|
||||
|
||||
```
|
||||
[STYLE]
|
||||
- **语言规范**:使用严谨、专业的中文进行技术写作。仅专业术语(如 Singleton, Middleware, ORM)保留英文原文。
|
||||
- **叙述视角**:采用完全客观的第三人称视角("上帝视角")。严禁使用"我们"、"开发者"、"用户"、"你"或"我"。主语应为"系统"、"模块"、"设计"、"架构"或"该层"。
|
||||
- **段落结构**:
|
||||
- 禁止使用无序列表作为主要叙述方式,必须将观点融合在连贯的段落中。
|
||||
- 采用"论点-论据-结论"的逻辑结构。
|
||||
- 善用逻辑连接词("因此"、"然而"、"鉴于"、"进而")来体现设计思路的推演过程。
|
||||
- **内容深度**:
|
||||
- 抽象化:描述"做什么"和"为什么这么做",而不是"怎么写的"。
|
||||
- 方法论:强调设计模式、架构原则(如 SOLID、高内聚低耦合)的应用。
|
||||
- 非代码化:除非定义关键接口,否则不直接引用代码。文件引用仅作为括号内的来源标注 (参考: path/to/file)。
|
||||
```
|
||||
|
||||
## Agent 配置
|
||||
|
||||
### Architecture Report Agents
|
||||
|
||||
| Agent | 输出文件 | 关注点 |
|
||||
|-------|----------|--------|
|
||||
| overview | section-overview.md | 顶层架构、技术决策、设计哲学 |
|
||||
| layers | section-layers.md | 逻辑分层、职责边界、隔离策略 |
|
||||
| dependencies | section-dependencies.md | 依赖治理、集成拓扑、风险控制 |
|
||||
| dataflow | section-dataflow.md | 数据流向、转换机制、一致性保障 |
|
||||
| entrypoints | section-entrypoints.md | 入口设计、调用链、异常传播 |
|
||||
|
||||
### Design Report Agents
|
||||
|
||||
| Agent | 输出文件 | 关注点 |
|
||||
|-------|----------|--------|
|
||||
| patterns | section-patterns.md | 架构模式、通信机制、横切关注点 |
|
||||
| classes | section-classes.md | 类型体系、继承策略、职责划分 |
|
||||
| interfaces | section-interfaces.md | 契约设计、抽象层次、扩展机制 |
|
||||
| state | section-state.md | 状态模型、生命周期、并发控制 |
|
||||
|
||||
### Methods Report Agents
|
||||
|
||||
| Agent | 输出文件 | 关注点 |
|
||||
|-------|----------|--------|
|
||||
| algorithms | section-algorithms.md | 核心算法思想、复杂度权衡、优化策略 |
|
||||
| paths | section-paths.md | 关键路径设计、性能敏感点、瓶颈分析 |
|
||||
| apis | section-apis.md | API 设计规范、版本策略、兼容性 |
|
||||
| logic | section-logic.md | 业务逻辑建模、决策机制、边界处理 |
|
||||
|
||||
---
|
||||
|
||||
## Agent 返回格式
|
||||
|
||||
```typescript
|
||||
interface AgentReturn {
|
||||
status: "completed" | "partial" | "failed";
|
||||
output_file: string;
|
||||
summary: string; // 50字以内
|
||||
cross_module_notes: string[]; // 跨模块发现
|
||||
stats: { diagrams: number; };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent 提示词
|
||||
|
||||
### Overview Agent
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
[ROLE] 首席系统架构师
|
||||
|
||||
[TASK]
|
||||
基于代码库的全貌,撰写《系统架构设计报告》的"总体架构"章节。透过代码表象,洞察系统的核心价值主张和顶层技术决策。
|
||||
输出: ${outDir}/sections/section-overview.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作,专业术语保留英文
|
||||
- 完全客观的第三人称视角,严禁"我们"、"开发者"
|
||||
- 段落式叙述,采用"论点-论据-结论"结构
|
||||
- 善用逻辑连接词体现设计推演过程
|
||||
- 描述"做什么"和"为什么",非"怎么写的"
|
||||
- 不直接引用代码,文件仅作来源标注
|
||||
|
||||
[FOCUS]
|
||||
- 领域边界与定位:系统旨在解决什么核心业务问题?其在更大的技术生态中处于什么位置?
|
||||
- 架构范式:采用何种架构风格(分层、六边形、微服务、事件驱动等)?选择该范式的根本原因是什么?
|
||||
- 核心技术决策:关键技术栈的选型依据,这些选型如何支撑系统的非功能性需求(性能、扩展性、维护性)
|
||||
- 顶层模块划分:系统在最高层级被划分为哪些逻辑单元?它们之间的高层协作机制是怎样的?
|
||||
|
||||
[CONSTRAINT]
|
||||
- 避免罗列目录结构
|
||||
- 重点阐述"设计意图"而非"现有功能"
|
||||
- 包含至少1个 Mermaid 架构图辅助说明
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-overview.md","summary":"<50字>","cross_module_notes":[],"stats":{"diagrams":1}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Layers Agent
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
[ROLE] 资深软件设计师
|
||||
|
||||
[TASK]
|
||||
分析系统的逻辑分层结构,撰写《系统架构设计报告》的"逻辑视点与分层架构"章节。重点揭示系统如何通过分层来隔离关注点。
|
||||
输出: ${outDir}/sections/section-layers.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作
|
||||
- 客观第三人称视角,主语为"系统"、"该层"、"设计"
|
||||
- 段落式叙述,禁止无序列表作为主体
|
||||
- 强调方法论和架构原则的应用
|
||||
|
||||
[FOCUS]
|
||||
- 职责分配体系:系统被划分为哪几个逻辑层级?每一层的核心职责和输入输出是什么?
|
||||
- 数据流向与约束:数据在各层之间是如何流动的?是否存在严格的单向依赖规则?
|
||||
- 边界隔离策略:各层之间通过何种方式解耦(接口抽象、DTO转换、依赖注入)?如何防止下层实现细节泄露到上层?
|
||||
- 异常处理流:异常信息如何在分层结构中传递和转化?
|
||||
|
||||
[CONSTRAINT]
|
||||
- 不要列举具体的文件名列表
|
||||
- 关注"层级间的契约"和"隔离的艺术"
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-layers.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Dependencies Agent
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
[ROLE] 集成架构专家
|
||||
|
||||
[TASK]
|
||||
审视系统的外部连接与内部耦合情况,撰写《系统架构设计报告》的"依赖管理与生态集成"章节。
|
||||
输出: ${outDir}/sections/section-dependencies.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作
|
||||
- 客观第三人称视角
|
||||
- 段落式叙述,逻辑连贯
|
||||
|
||||
[FOCUS]
|
||||
- 外部集成拓扑:系统如何与外部世界(第三方API、数据库、中间件)交互?采用了何种适配器或防腐层设计来隔离外部变化?
|
||||
- 核心依赖分析:区分"核心业务依赖"与"基础设施依赖"。系统对关键框架的依赖程度如何?是否存在被锁定的风险?
|
||||
- 依赖注入与控制反转:系统内部模块间的组装方式是什么?是否实现了依赖倒置原则以支持可测试性?
|
||||
- 供应链安全与治理:对于复杂的依赖树,系统采用了何种策略来管理版本和兼容性?
|
||||
|
||||
[CONSTRAINT]
|
||||
- 禁止简单列出依赖配置文件的内容
|
||||
- 必须分析依赖背后的"集成策略"和"风险控制模型"
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-dependencies.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Patterns Agent
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
[ROLE] 核心开发规范制定者
|
||||
|
||||
[TASK]
|
||||
挖掘代码中的复用机制和标准化实践,撰写《系统架构设计报告》的"设计模式与工程规范"章节。
|
||||
输出: ${outDir}/sections/section-patterns.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作
|
||||
- 客观第三人称视角
|
||||
- 段落式叙述,结合项目上下文
|
||||
|
||||
[FOCUS]
|
||||
- 架构级模式:识别系统中广泛使用的架构模式(CQRS、Event Sourcing、Repository Pattern、Unit of Work)。阐述引入这些模式解决了什么特定难题
|
||||
- 通信与并发模式:分析组件间的通信机制(同步/异步、观察者模式、发布订阅)以及并发控制策略
|
||||
- 横切关注点实现:系统如何统一处理日志、鉴权、缓存、事务管理等横切逻辑(AOP、中间件管道、装饰器)?
|
||||
- 抽象与复用策略:分析基类、泛型、工具类的设计思想,系统如何通过抽象来减少重复代码并提高一致性?
|
||||
|
||||
[CONSTRAINT]
|
||||
- 避免教科书式地解释设计模式定义,必须结合当前项目上下文说明其应用场景
|
||||
- 关注"解决类问题的通用机制"
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-patterns.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### DataFlow Agent
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
[ROLE] 数据架构师
|
||||
|
||||
[TASK]
|
||||
追踪系统的数据流转机制,撰写《系统架构设计报告》的"数据流与状态管理"章节。
|
||||
输出: ${outDir}/sections/section-dataflow.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作
|
||||
- 客观第三人称视角
|
||||
- 段落式叙述
|
||||
|
||||
[FOCUS]
|
||||
- 数据入口与出口:数据从何处进入系统,最终流向何处?边界处的数据校验和转换策略是什么?
|
||||
- 数据转换管道:数据在各层/模块间经历了怎样的形态变化?DTO、Entity、VO 等数据对象的职责边界如何划分?
|
||||
- 持久化策略:系统如何设计数据存储方案?采用了何种 ORM 策略或数据访问模式?
|
||||
- 一致性保障:系统如何处理事务边界?分布式场景下如何保证数据一致性?
|
||||
|
||||
[CONSTRAINT]
|
||||
- 关注数据的"生命周期"和"形态演变"
|
||||
- 不要罗列数据库表结构
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-dataflow.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### EntryPoints Agent
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
[ROLE] 系统边界分析师
|
||||
|
||||
[TASK]
|
||||
识别系统的入口设计和关键路径,撰写《系统架构设计报告》的"系统入口与调用链"章节。
|
||||
输出: ${outDir}/sections/section-entrypoints.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作
|
||||
- 客观第三人称视角
|
||||
- 段落式叙述
|
||||
|
||||
[FOCUS]
|
||||
- 入口类型与职责:系统提供了哪些类型的入口(REST API、CLI、消息队列消费者、定时任务)?各入口的设计目的和适用场景是什么?
|
||||
- 请求处理管道:从入口到核心逻辑,请求经过了怎样的处理管道?中间件/拦截器的编排逻辑是什么?
|
||||
- 关键业务路径:最重要的几条业务流程的调用链是怎样的?关键节点的设计考量是什么?
|
||||
- 异常与边界处理:系统如何统一处理异常?异常信息如何传播和转化?
|
||||
|
||||
[CONSTRAINT]
|
||||
- 关注"入口的设计哲学"而非 API 清单
|
||||
- 不要逐个列举所有端点
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-entrypoints.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Classes Agent
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
[ROLE] 领域模型设计师
|
||||
|
||||
[TASK]
|
||||
分析系统的类型体系和领域模型,撰写《系统架构设计报告》的"类型体系与领域建模"章节。
|
||||
输出: ${outDir}/sections/section-classes.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作
|
||||
- 客观第三人称视角
|
||||
- 段落式叙述
|
||||
|
||||
[FOCUS]
|
||||
- 领域模型设计:系统的核心领域概念有哪些?它们之间的关系如何建模(聚合、实体、值对象)?
|
||||
- 继承与组合策略:系统倾向于使用继承还是组合?基类/接口的设计意图是什么?
|
||||
- 职责分配原则:类的职责划分遵循了什么原则?是否体现了单一职责原则?
|
||||
- 类型安全与约束:系统如何利用类型系统来表达业务约束和不变量?
|
||||
|
||||
[CONSTRAINT]
|
||||
- 关注"建模思想"而非类的属性列表
|
||||
- 用 UML 类图辅助说明核心关系
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-classes.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Interfaces Agent
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
[ROLE] 契约设计专家
|
||||
|
||||
[TASK]
|
||||
分析系统的接口设计和抽象层次,撰写《系统架构设计报告》的"接口契约与抽象设计"章节。
|
||||
输出: ${outDir}/sections/section-interfaces.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作
|
||||
- 客观第三人称视角
|
||||
- 段落式叙述
|
||||
|
||||
[FOCUS]
|
||||
- 抽象层次设计:系统定义了哪些核心接口/抽象类?这些抽象的设计意图和职责边界是什么?
|
||||
- 契约与实现分离:接口如何隔离契约与实现?多态机制如何被运用?
|
||||
- 扩展点设计:系统预留了哪些扩展点?如何在不修改核心代码的情况下扩展功能?
|
||||
- 版本演进策略:接口如何支持版本演进?向后兼容性如何保障?
|
||||
|
||||
[CONSTRAINT]
|
||||
- 关注"接口的设计哲学"
|
||||
- 不要逐个列举接口方法签名
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-interfaces.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### State Agent
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
[ROLE] 状态管理架构师
|
||||
|
||||
[TASK]
|
||||
分析系统的状态管理机制,撰写《系统架构设计报告》的"状态管理与生命周期"章节。
|
||||
输出: ${outDir}/sections/section-state.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作
|
||||
- 客观第三人称视角
|
||||
- 段落式叙述
|
||||
|
||||
[FOCUS]
|
||||
- 状态模型设计:系统需要管理哪些类型的状态(会话状态、应用状态、领域状态)?状态的存储位置和作用域是什么?
|
||||
- 状态生命周期:状态如何创建、更新、销毁?生命周期管理的机制是什么?
|
||||
- 并发与一致性:多线程/多实例场景下,状态如何保持一致?采用了何种并发控制策略?
|
||||
- 状态恢复与容错:系统如何处理状态丢失或损坏?是否有状态恢复机制?
|
||||
|
||||
[CONSTRAINT]
|
||||
- 关注"状态管理的设计决策"
|
||||
- 不要列举具体的变量名
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-state.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Algorithms Agent
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
[ROLE] 算法架构师
|
||||
|
||||
[TASK]
|
||||
分析系统的核心算法设计,撰写《系统架构设计报告》的"核心算法与计算模型"章节。
|
||||
输出: ${outDir}/sections/section-algorithms.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作
|
||||
- 客观第三人称视角
|
||||
- 段落式叙述
|
||||
|
||||
[FOCUS]
|
||||
- 算法选型与权衡:系统的核心业务逻辑采用了哪些关键算法?选择这些算法的考量因素是什么(时间复杂度、空间复杂度、可维护性)?
|
||||
- 计算模型设计:复杂计算如何被分解和组织?是否采用了流水线、Map-Reduce 等计算模式?
|
||||
- 性能与可扩展性:算法设计如何考虑性能和可扩展性?是否有针对大数据量的优化策略?
|
||||
- 正确性保障:关键算法的正确性如何保障?是否有边界条件的特殊处理?
|
||||
|
||||
[CONSTRAINT]
|
||||
- 关注"算法思想"而非具体实现代码
|
||||
- 用流程图辅助说明复杂逻辑
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-algorithms.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Paths Agent
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
[ROLE] 性能架构师
|
||||
|
||||
[TASK]
|
||||
分析系统的关键执行路径,撰写《系统架构设计报告》的"关键路径与性能设计"章节。
|
||||
输出: ${outDir}/sections/section-paths.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作
|
||||
- 客观第三人称视角
|
||||
- 段落式叙述
|
||||
|
||||
[FOCUS]
|
||||
- 关键业务路径:系统中最重要的几条业务执行路径是什么?这些路径的设计目标和约束是什么?
|
||||
- 性能敏感区域:哪些环节是性能敏感的?系统采用了何种优化策略(缓存、异步、批处理)?
|
||||
- 瓶颈识别与缓解:潜在的性能瓶颈在哪里?设计中是否预留了扩展空间?
|
||||
- 降级与熔断:在高负载或故障场景下,系统如何保护关键路径?
|
||||
|
||||
[CONSTRAINT]
|
||||
- 关注"路径设计的战略考量"
|
||||
- 不要罗列所有代码执行步骤
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-paths.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### APIs Agent
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
[ROLE] API 设计规范专家
|
||||
|
||||
[TASK]
|
||||
分析系统的对外接口设计规范,撰写《系统架构设计报告》的"API 设计与规范"章节。
|
||||
输出: ${outDir}/sections/section-apis.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作
|
||||
- 客观第三人称视角
|
||||
- 段落式叙述
|
||||
|
||||
[FOCUS]
|
||||
- API 设计风格:系统采用了何种 API 设计风格(RESTful、GraphQL、RPC)?选择该风格的原因是什么?
|
||||
- 命名与结构规范:API 的命名、路径结构、参数设计遵循了什么规范?是否有一致性保障机制?
|
||||
- 版本管理策略:API 如何支持版本演进?向后兼容性策略是什么?
|
||||
- 错误处理规范:API 错误响应的设计规范是什么?错误码体系如何组织?
|
||||
|
||||
[CONSTRAINT]
|
||||
- 关注"设计规范和一致性"
|
||||
- 不要逐个列举所有 API 端点
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-apis.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Logic Agent
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
[SPEC]
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
[ROLE] 业务逻辑架构师
|
||||
|
||||
[TASK]
|
||||
分析系统的业务逻辑建模,撰写《系统架构设计报告》的"业务逻辑与规则引擎"章节。
|
||||
输出: ${outDir}/sections/section-logic.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作
|
||||
- 客观第三人称视角
|
||||
- 段落式叙述
|
||||
|
||||
[FOCUS]
|
||||
- 业务规则建模:核心业务规则如何被表达和组织?是否采用了规则引擎或策略模式?
|
||||
- 决策点设计:系统中的关键决策点有哪些?决策逻辑如何被封装和测试?
|
||||
- 边界条件处理:系统如何处理边界条件和异常情况?是否有防御性编程措施?
|
||||
- 业务流程编排:复杂业务流程如何被编排?是否采用了工作流引擎或状态机?
|
||||
|
||||
[CONSTRAINT]
|
||||
- 关注"业务逻辑的组织方式"
|
||||
- 不要逐行解释代码逻辑
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-logic.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 执行流程
|
||||
|
||||
```javascript
|
||||
// 1. 根据报告类型选择 Agent 配置
|
||||
const agentConfigs = getAgentConfigs(config.type);
|
||||
|
||||
// 2. 准备目录
|
||||
Bash(`mkdir "${outputDir}\\sections"`);
|
||||
|
||||
// 3. 并行启动所有 Agent
|
||||
const results = await Promise.all(
|
||||
agentConfigs.map(agent => launchAgent(agent, config, outputDir))
|
||||
);
|
||||
|
||||
// 4. 收集简要返回信息
|
||||
const summaries = results.map(r => JSON.parse(r));
|
||||
|
||||
// 5. 传递给 Phase 3.5 汇总 Agent
|
||||
return { summaries, cross_notes: summaries.flatMap(s => s.cross_module_notes) };
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
各 Agent 写入 `sections/section-xxx.md`,返回简要 JSON 供 Phase 3.5 汇总。
|
||||
208
.claude/skills/project-analyze/phases/03.5-consolidation.md
Normal file
208
.claude/skills/project-analyze/phases/03.5-consolidation.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Phase 3.5: Consolidation Agent
|
||||
|
||||
汇总所有分析 Agent 的产出,生成跨章节综合分析,为 Phase 4 索引报告提供内容。
|
||||
|
||||
> **写作规范**: [../specs/writing-style.md](../specs/writing-style.md)
|
||||
|
||||
## 核心职责
|
||||
|
||||
1. **跨章节综合分析**:生成 synthesis(报告综述)
|
||||
2. **章节摘要提取**:生成 section_summaries(索引表格内容)
|
||||
3. **质量检查**:识别问题并评分
|
||||
4. **建议汇总**:生成 recommendations(优先级排序)
|
||||
|
||||
## 输入
|
||||
|
||||
```typescript
|
||||
interface ConsolidationInput {
|
||||
output_dir: string;
|
||||
config: AnalysisConfig;
|
||||
agent_summaries: AgentReturn[];
|
||||
cross_module_notes: string[];
|
||||
}
|
||||
```
|
||||
|
||||
## 执行
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
prompt: `
|
||||
## 规范前置
|
||||
首先读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
严格遵循规范中的质量标准和段落式写作要求。
|
||||
|
||||
## 任务
|
||||
作为汇总 Agent,读取所有章节文件,执行跨章节分析,生成汇总报告和索引内容。
|
||||
|
||||
## 输入
|
||||
- 章节文件: ${outputDir}/sections/section-*.md
|
||||
- Agent 摘要: ${JSON.stringify(agent_summaries)}
|
||||
- 跨模块备注: ${JSON.stringify(cross_module_notes)}
|
||||
- 报告类型: ${config.type}
|
||||
|
||||
## 核心产出
|
||||
|
||||
### 1. 综合分析 (synthesis)
|
||||
阅读所有章节,用 2-3 段落描述项目全貌:
|
||||
- 第一段:项目定位与核心架构特征
|
||||
- 第二段:关键设计决策与技术选型
|
||||
- 第三段:整体质量评价与显著特点
|
||||
|
||||
### 2. 章节摘要 (section_summaries)
|
||||
为每个章节提取一句话核心发现,用于索引表格。
|
||||
|
||||
### 3. 架构洞察 (cross_analysis)
|
||||
描述章节间的关联性,如:
|
||||
- 模块间的依赖关系如何体现在各章节
|
||||
- 设计决策如何贯穿多个层面
|
||||
- 潜在的一致性或冲突
|
||||
|
||||
### 4. 建议汇总 (recommendations)
|
||||
按优先级整理各章节的建议,段落式描述。
|
||||
|
||||
## 质量检查维度
|
||||
|
||||
### 一致性检查
|
||||
- 术语一致性:同一概念是否使用相同名称
|
||||
- 代码引用:file:line 格式是否正确
|
||||
|
||||
### 完整性检查
|
||||
- 章节覆盖:是否涵盖所有必需章节
|
||||
- 内容深度:每章节是否达到 ${config.depth} 级别
|
||||
|
||||
### 质量检查
|
||||
- Mermaid 语法:图表是否可渲染
|
||||
- 段落式写作:是否符合写作规范(禁止清单罗列)
|
||||
|
||||
## 输出文件
|
||||
|
||||
写入: ${outputDir}/consolidation-summary.md
|
||||
|
||||
### 文件格式
|
||||
|
||||
\`\`\`markdown
|
||||
# 分析汇总报告
|
||||
|
||||
## 综合分析
|
||||
|
||||
[2-3 段落的项目全貌描述,段落式写作]
|
||||
|
||||
## 章节摘要
|
||||
|
||||
| 章节 | 文件 | 核心发现 |
|
||||
|------|------|----------|
|
||||
| 系统概述 | section-overview.md | 一句话描述 |
|
||||
| 层次分析 | section-layers.md | 一句话描述 |
|
||||
| ... | ... | ... |
|
||||
|
||||
## 架构洞察
|
||||
|
||||
[跨章节关联分析,段落式描述]
|
||||
|
||||
## 建议汇总
|
||||
|
||||
[优先级排序的建议,段落式描述]
|
||||
|
||||
---
|
||||
|
||||
## 质量评估
|
||||
|
||||
### 评分
|
||||
|
||||
| 维度 | 得分 | 说明 |
|
||||
|------|------|------|
|
||||
| 完整性 | 85% | ... |
|
||||
| 一致性 | 90% | ... |
|
||||
| 深度 | 95% | ... |
|
||||
| 可读性 | 88% | ... |
|
||||
| 综合 | 89% | ... |
|
||||
|
||||
### 发现的问题
|
||||
|
||||
#### 严重问题
|
||||
| ID | 类型 | 位置 | 描述 |
|
||||
|----|------|------|------|
|
||||
| E001 | ... | ... | ... |
|
||||
|
||||
#### 警告
|
||||
| ID | 类型 | 位置 | 描述 |
|
||||
|----|------|------|------|
|
||||
| W001 | ... | ... | ... |
|
||||
|
||||
#### 提示
|
||||
| ID | 类型 | 位置 | 描述 |
|
||||
|----|------|------|------|
|
||||
| I001 | ... | ... | ... |
|
||||
|
||||
### 统计
|
||||
|
||||
- 章节数: X
|
||||
- 图表数: X
|
||||
- 总字数: X
|
||||
\`\`\`
|
||||
|
||||
## 返回格式 (JSON)
|
||||
|
||||
{
|
||||
"status": "completed",
|
||||
"output_file": "consolidation-summary.md",
|
||||
|
||||
// Phase 4 索引报告所需
|
||||
"synthesis": "2-3 段落的综合分析文本",
|
||||
"cross_analysis": "跨章节关联分析文本",
|
||||
"recommendations": "优先级排序的建议文本",
|
||||
"section_summaries": [
|
||||
{"file": "section-overview.md", "title": "系统概述", "summary": "一句话核心发现"},
|
||||
{"file": "section-layers.md", "title": "层次分析", "summary": "一句话核心发现"}
|
||||
],
|
||||
|
||||
// 质量信息
|
||||
"quality_score": {
|
||||
"completeness": 85,
|
||||
"consistency": 90,
|
||||
"depth": 95,
|
||||
"readability": 88,
|
||||
"overall": 89
|
||||
},
|
||||
"issues": {
|
||||
"errors": [...],
|
||||
"warnings": [...],
|
||||
"info": [...]
|
||||
},
|
||||
"stats": {
|
||||
"total_sections": 5,
|
||||
"total_diagrams": 8,
|
||||
"total_words": 3500
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
## 问题分类
|
||||
|
||||
| 严重级别 | 前缀 | 含义 | 处理方式 |
|
||||
|----------|------|------|----------|
|
||||
| Error | E | 阻塞报告生成 | 必须修复 |
|
||||
| Warning | W | 影响报告质量 | 建议修复 |
|
||||
| Info | I | 可改进项 | 可选修复 |
|
||||
|
||||
## 问题类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| missing | 缺失章节 |
|
||||
| inconsistency | 术语/描述不一致 |
|
||||
| invalid_ref | 无效代码引用 |
|
||||
| syntax | Mermaid 语法错误 |
|
||||
| shallow | 内容过浅 |
|
||||
| list_style | 违反段落式写作规范 |
|
||||
|
||||
## Output
|
||||
|
||||
- **文件**: `consolidation-summary.md`(完整汇总报告)
|
||||
- **返回**: JSON 包含 Phase 4 所需的所有字段
|
||||
217
.claude/skills/project-analyze/phases/04-report-generation.md
Normal file
217
.claude/skills/project-analyze/phases/04-report-generation.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# Phase 4: Report Generation
|
||||
|
||||
生成索引式报告,通过 markdown 链接引用章节文件。
|
||||
|
||||
> **规范参考**: [../specs/quality-standards.md](../specs/quality-standards.md)
|
||||
|
||||
## 设计原则
|
||||
|
||||
1. **引用而非嵌入**:主报告通过链接引用章节,不复制内容
|
||||
2. **索引 + 综述**:主报告提供导航和高阶分析
|
||||
3. **避免重复**:综述来自 consolidation,不重新生成
|
||||
4. **独立可读**:各章节文件可单独阅读
|
||||
|
||||
## 输入
|
||||
|
||||
```typescript
|
||||
interface ReportInput {
|
||||
output_dir: string;
|
||||
config: AnalysisConfig;
|
||||
consolidation: {
|
||||
quality_score: QualityScore;
|
||||
issues: { errors: Issue[], warnings: Issue[], info: Issue[] };
|
||||
stats: Stats;
|
||||
synthesis: string; // consolidation agent 的综合分析
|
||||
section_summaries: Array<{file: string, summary: string}>;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
```javascript
|
||||
// 1. 质量门禁检查
|
||||
if (consolidation.issues.errors.length > 0) {
|
||||
const response = await AskUserQuestion({
|
||||
questions: [{
|
||||
question: `发现 ${consolidation.issues.errors.length} 个严重问题,如何处理?`,
|
||||
header: "质量检查",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{label: "查看并修复", description: "显示问题列表,手动修复后重试"},
|
||||
{label: "忽略继续", description: "跳过问题检查,继续装配"},
|
||||
{label: "终止", description: "停止报告生成"}
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
if (response === "查看并修复") {
|
||||
return { action: "fix_required", errors: consolidation.issues.errors };
|
||||
}
|
||||
if (response === "终止") {
|
||||
return { action: "abort" };
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 生成索引式报告(不读取章节内容)
|
||||
const report = generateIndexReport(config, consolidation);
|
||||
|
||||
// 3. 写入最终文件
|
||||
const fileName = `${config.type.toUpperCase()}-REPORT.md`;
|
||||
Write(`${outputDir}/${fileName}`, report);
|
||||
```
|
||||
|
||||
## 报告模板
|
||||
|
||||
### 通用结构
|
||||
|
||||
```markdown
|
||||
# {报告标题}
|
||||
|
||||
> 生成日期:{date}
|
||||
> 分析范围:{scope}
|
||||
> 分析深度:{depth}
|
||||
> 质量评分:{overall}%
|
||||
|
||||
---
|
||||
|
||||
## 报告综述
|
||||
|
||||
{consolidation.synthesis - 来自汇总 Agent 的跨章节综合分析}
|
||||
|
||||
---
|
||||
|
||||
## 章节索引
|
||||
|
||||
| 章节 | 核心发现 | 详情 |
|
||||
|------|----------|------|
|
||||
{section_summaries 生成的表格行}
|
||||
|
||||
---
|
||||
|
||||
## 架构洞察
|
||||
|
||||
{从 consolidation 提取的跨模块关联分析}
|
||||
|
||||
---
|
||||
|
||||
## 建议与展望
|
||||
|
||||
{consolidation.recommendations - 优先级排序的综合建议}
|
||||
|
||||
---
|
||||
|
||||
**附录**
|
||||
|
||||
- [质量报告](./consolidation-summary.md)
|
||||
- [章节文件目录](./sections/)
|
||||
```
|
||||
|
||||
### 报告标题映射
|
||||
|
||||
| 类型 | 标题 |
|
||||
|------|------|
|
||||
| architecture | 项目架构设计报告 |
|
||||
| design | 项目设计模式报告 |
|
||||
| methods | 项目核心方法报告 |
|
||||
| comprehensive | 项目综合分析报告 |
|
||||
|
||||
## 生成函数
|
||||
|
||||
```javascript
|
||||
function generateIndexReport(config, consolidation) {
|
||||
const titles = {
|
||||
architecture: "项目架构设计报告",
|
||||
design: "项目设计模式报告",
|
||||
methods: "项目核心方法报告",
|
||||
comprehensive: "项目综合分析报告"
|
||||
};
|
||||
|
||||
const date = new Date().toLocaleDateString('zh-CN');
|
||||
|
||||
// 章节索引表格
|
||||
const sectionTable = consolidation.section_summaries
|
||||
.map(s => `| ${s.title} | ${s.summary} | [查看详情](./sections/${s.file}) |`)
|
||||
.join('\n');
|
||||
|
||||
return `# ${titles[config.type]}
|
||||
|
||||
> 生成日期:${date}
|
||||
> 分析范围:${config.scope}
|
||||
> 分析深度:${config.depth}
|
||||
> 质量评分:${consolidation.quality_score.overall}%
|
||||
|
||||
---
|
||||
|
||||
## 报告综述
|
||||
|
||||
${consolidation.synthesis}
|
||||
|
||||
---
|
||||
|
||||
## 章节索引
|
||||
|
||||
| 章节 | 核心发现 | 详情 |
|
||||
|------|----------|------|
|
||||
${sectionTable}
|
||||
|
||||
---
|
||||
|
||||
## 架构洞察
|
||||
|
||||
${consolidation.cross_analysis || '详见各章节分析。'}
|
||||
|
||||
---
|
||||
|
||||
## 建议与展望
|
||||
|
||||
${consolidation.recommendations || '详见质量报告中的改进建议。'}
|
||||
|
||||
---
|
||||
|
||||
**附录**
|
||||
|
||||
- [质量报告](./consolidation-summary.md)
|
||||
- [章节文件目录](./sections/)
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
## 输出结构
|
||||
|
||||
```
|
||||
.workflow/.scratchpad/analyze-{timestamp}/
|
||||
├── sections/ # 独立章节(Phase 3 产出)
|
||||
│ ├── section-overview.md
|
||||
│ ├── section-layers.md
|
||||
│ └── ...
|
||||
├── consolidation-summary.md # 质量报告(Phase 3.5 产出)
|
||||
└── {TYPE}-REPORT.md # 索引报告(本阶段产出)
|
||||
```
|
||||
|
||||
## 与 Phase 3.5 的协作
|
||||
|
||||
Phase 3.5 consolidation agent 需要提供:
|
||||
|
||||
```typescript
|
||||
interface ConsolidationOutput {
|
||||
// ... 原有字段
|
||||
synthesis: string; // 跨章节综合分析(2-3 段落)
|
||||
cross_analysis: string; // 架构级关联洞察
|
||||
recommendations: string; // 优先级排序的建议
|
||||
section_summaries: Array<{
|
||||
file: string; // 文件名
|
||||
title: string; // 章节标题
|
||||
summary: string; // 一句话核心发现
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
## 关键变更
|
||||
|
||||
| 原设计 | 新设计 |
|
||||
|--------|--------|
|
||||
| 读取章节内容并拼接 | 链接引用,不读取内容 |
|
||||
| 重新生成 Executive Summary | 直接使用 consolidation.synthesis |
|
||||
| 嵌入质量评分表格 | 链接引用 consolidation-summary.md |
|
||||
| 主报告包含全部内容 | 主报告仅为索引 + 综述 |
|
||||
124
.claude/skills/project-analyze/phases/05-iterative-refinement.md
Normal file
124
.claude/skills/project-analyze/phases/05-iterative-refinement.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Phase 5: Iterative Refinement
|
||||
|
||||
Discovery-driven refinement based on analysis findings.
|
||||
|
||||
## Execution
|
||||
|
||||
### Step 1: Extract Discoveries
|
||||
|
||||
```javascript
|
||||
function extractDiscoveries(deepAnalysis) {
|
||||
return {
|
||||
ambiguities: deepAnalysis.findings.filter(f => f.confidence < 0.7),
|
||||
complexityHotspots: deepAnalysis.findings.filter(f => f.complexity === 'high'),
|
||||
patternDeviations: deepAnalysis.patterns.filter(p => p.consistency < 0.8),
|
||||
unclearDependencies: deepAnalysis.dependencies.filter(d => d.type === 'implicit'),
|
||||
potentialIssues: deepAnalysis.recommendations.filter(r => r.priority === 'investigate'),
|
||||
depthOpportunities: deepAnalysis.sections.filter(s => s.has_more_detail)
|
||||
};
|
||||
}
|
||||
|
||||
const discoveries = extractDiscoveries(deepAnalysis);
|
||||
```
|
||||
|
||||
### Step 2: Build Dynamic Questions
|
||||
|
||||
Questions emerge from discoveries, NOT predetermined:
|
||||
|
||||
```javascript
|
||||
function buildDynamicQuestions(discoveries, config) {
|
||||
const questions = [];
|
||||
|
||||
if (discoveries.ambiguities.length > 0) {
|
||||
questions.push({
|
||||
question: `Analysis found ambiguity in "${discoveries.ambiguities[0].area}". Which interpretation is correct?`,
|
||||
header: "Clarify",
|
||||
options: discoveries.ambiguities[0].interpretations
|
||||
});
|
||||
}
|
||||
|
||||
if (discoveries.complexityHotspots.length > 0) {
|
||||
questions.push({
|
||||
question: `These areas have high complexity. Which would you like explained?`,
|
||||
header: "Deep-Dive",
|
||||
multiSelect: true,
|
||||
options: discoveries.complexityHotspots.slice(0, 4).map(h => ({
|
||||
label: h.name,
|
||||
description: h.summary
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
if (discoveries.patternDeviations.length > 0) {
|
||||
questions.push({
|
||||
question: `Found pattern deviations. Should these be highlighted in the report?`,
|
||||
header: "Patterns",
|
||||
options: [
|
||||
{label: "Yes, include analysis", description: "Add section explaining deviations"},
|
||||
{label: "No, skip", description: "Omit from report"}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Always include action question
|
||||
questions.push({
|
||||
question: "How would you like to proceed?",
|
||||
header: "Action",
|
||||
options: [
|
||||
{label: "Continue refining", description: "Address more discoveries"},
|
||||
{label: "Finalize report", description: "Generate final output"},
|
||||
{label: "Change scope", description: "Modify analysis scope"}
|
||||
]
|
||||
});
|
||||
|
||||
return questions.slice(0, 4); // Max 4 questions
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Apply Refinements
|
||||
|
||||
```javascript
|
||||
if (userAction === "Continue refining") {
|
||||
// Apply selected refinements
|
||||
for (const selection of userSelections) {
|
||||
applyRefinement(selection, deepAnalysis, report);
|
||||
}
|
||||
|
||||
// Save iteration
|
||||
Write(`${outputDir}/iterations/iteration-${iterationCount}.json`, {
|
||||
timestamp: new Date().toISOString(),
|
||||
discoveries: discoveries,
|
||||
selections: userSelections,
|
||||
changes: appliedChanges
|
||||
});
|
||||
|
||||
// Loop back to Step 1
|
||||
iterationCount++;
|
||||
goto Step1;
|
||||
}
|
||||
|
||||
if (userAction === "Finalize report") {
|
||||
// Proceed to final output
|
||||
goto FinalizeReport;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Finalize Report
|
||||
|
||||
```javascript
|
||||
// Add iteration history to report metadata
|
||||
const finalReport = {
|
||||
...report,
|
||||
metadata: {
|
||||
iterations: iterationCount,
|
||||
refinements_applied: allRefinements,
|
||||
final_discoveries: discoveries
|
||||
}
|
||||
};
|
||||
|
||||
Write(`${outputDir}/${config.type.toUpperCase()}-REPORT.md`, finalReport);
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Updated report with refinements, saved iterations to `iterations/` folder.
|
||||
115
.claude/skills/project-analyze/specs/quality-standards.md
Normal file
115
.claude/skills/project-analyze/specs/quality-standards.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Quality Standards
|
||||
|
||||
Quality gates and requirements for project analysis reports.
|
||||
|
||||
## When to Use
|
||||
|
||||
| Phase | Usage | Section |
|
||||
|-------|-------|---------|
|
||||
| Phase 4 | Check report structure before assembly | Report Requirements |
|
||||
| Phase 5 | Validate before each iteration | Quality Gates |
|
||||
| Phase 5 | Handle failures during refinement | Error Handling |
|
||||
|
||||
---
|
||||
|
||||
## Report Requirements
|
||||
|
||||
**Use in Phase 4**: Ensure report includes all required elements.
|
||||
|
||||
| Requirement | Check | How to Fix |
|
||||
|-------------|-------|------------|
|
||||
| Executive Summary | 3-5 key takeaways | Extract from analysis findings |
|
||||
| Visual diagrams | Valid Mermaid syntax | Use `../_shared/mermaid-utils.md` |
|
||||
| Code references | `file:line` format | Link to actual source locations |
|
||||
| Recommendations | Actionable, specific | Derive from analysis insights |
|
||||
| Consistent depth | Match user's depth level | Adjust detail per config.depth |
|
||||
|
||||
---
|
||||
|
||||
## Quality Gates
|
||||
|
||||
**Use in Phase 5**: Run these checks before asking user questions.
|
||||
|
||||
```javascript
|
||||
function runQualityGates(report, config, diagrams) {
|
||||
const gates = [
|
||||
{
|
||||
name: "focus_areas_covered",
|
||||
check: () => config.focus_areas.every(area =>
|
||||
report.toLowerCase().includes(area.toLowerCase())
|
||||
),
|
||||
fix: "Re-analyze missing focus areas"
|
||||
},
|
||||
{
|
||||
name: "diagrams_valid",
|
||||
check: () => diagrams.every(d => d.valid),
|
||||
fix: "Regenerate failed diagrams with mermaid-utils"
|
||||
},
|
||||
{
|
||||
name: "code_refs_accurate",
|
||||
check: () => extractCodeRefs(report).every(ref => fileExists(ref)),
|
||||
fix: "Update invalid file references"
|
||||
},
|
||||
{
|
||||
name: "no_placeholders",
|
||||
check: () => !report.includes('[TODO]') && !report.includes('[PLACEHOLDER]'),
|
||||
fix: "Fill in all placeholder content"
|
||||
},
|
||||
{
|
||||
name: "recommendations_specific",
|
||||
check: () => !report.includes('consider') || report.includes('specifically'),
|
||||
fix: "Make recommendations project-specific"
|
||||
}
|
||||
];
|
||||
|
||||
const results = gates.map(g => ({...g, passed: g.check()}));
|
||||
const allPassed = results.every(r => r.passed);
|
||||
|
||||
return { allPassed, results };
|
||||
}
|
||||
```
|
||||
|
||||
**Integration with Phase 5**:
|
||||
```javascript
|
||||
// In 05-iterative-refinement.md
|
||||
const { allPassed, results } = runQualityGates(report, config, diagrams);
|
||||
|
||||
if (allPassed) {
|
||||
// All gates passed → ask user to confirm or finalize
|
||||
} else {
|
||||
// Gates failed → include failed gates in discovery questions
|
||||
const failedGates = results.filter(r => !r.passed);
|
||||
discoveries.qualityIssues = failedGates;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Use when**: Encountering errors during any phase.
|
||||
|
||||
| Error | Detection | Recovery |
|
||||
|-------|-----------|----------|
|
||||
| CLI timeout | Bash exits with timeout | Reduce scope via `config.scope`, retry |
|
||||
| Exploration failure | Agent returns error | Fall back to `Read` + `Grep` directly |
|
||||
| User abandons | User selects "cancel" | Save to `iterations/`, allow resume |
|
||||
| Invalid scope path | Path doesn't exist | `AskUserQuestion` to correct path |
|
||||
| Diagram validation fails | `validateMermaidSyntax` returns issues | Regenerate with stricter escaping |
|
||||
|
||||
**Recovery Flow**:
|
||||
```javascript
|
||||
try {
|
||||
await executePhase(phase);
|
||||
} catch (error) {
|
||||
const recovery = ERROR_HANDLERS[error.type];
|
||||
if (recovery) {
|
||||
await recovery.action(error, config);
|
||||
// Retry phase or continue
|
||||
} else {
|
||||
// Save progress and ask user
|
||||
Write(`${outputDir}/error-state.json`, { phase, error, config });
|
||||
AskUserQuestion({ question: "遇到错误,如何处理?", ... });
|
||||
}
|
||||
}
|
||||
```
|
||||
152
.claude/skills/project-analyze/specs/writing-style.md
Normal file
152
.claude/skills/project-analyze/specs/writing-style.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# 写作风格规范
|
||||
|
||||
## 核心原则
|
||||
|
||||
**段落式描述,层层递进,禁止清单罗列。**
|
||||
|
||||
## 禁止的写作模式
|
||||
|
||||
```markdown
|
||||
<!-- 禁止:清单罗列 -->
|
||||
### 模块列表
|
||||
- 用户模块:处理用户相关功能
|
||||
- 订单模块:处理订单相关功能
|
||||
- 支付模块:处理支付相关功能
|
||||
|
||||
### 依赖关系
|
||||
| 模块 | 依赖 | 说明 |
|
||||
|------|------|------|
|
||||
| A | B | xxx |
|
||||
```
|
||||
|
||||
## 推荐的写作模式
|
||||
|
||||
```markdown
|
||||
<!-- 推荐:段落式描述 -->
|
||||
### 模块架构设计
|
||||
|
||||
系统采用分层模块化架构,核心业务逻辑围绕用户、订单、支付三大领域展开。
|
||||
用户模块作为系统的入口层,承担身份认证与权限管理职责,为下游模块提供
|
||||
统一的用户上下文。订单模块位于业务核心层,依赖用户模块获取会话信息,
|
||||
并协调支付模块完成交易闭环。
|
||||
|
||||
值得注意的是,支付模块采用策略模式实现多渠道支付,通过接口抽象与
|
||||
具体支付网关解耦。这一设计使得新增支付渠道时,仅需实现相应策略类,
|
||||
无需修改核心订单逻辑,体现了开闭原则的应用。
|
||||
|
||||
从依赖方向分析,系统呈现清晰的单向依赖:表现层依赖业务层,业务层
|
||||
依赖数据层,未发现循环依赖。这一架构特征确保了模块的独立可测试性,
|
||||
同时为后续微服务拆分奠定了基础。
|
||||
```
|
||||
|
||||
## 写作策略
|
||||
|
||||
### 策略一:主语转换
|
||||
|
||||
将主语从开发者视角转移到系统/代码本身:
|
||||
|
||||
| 禁止 | 推荐 |
|
||||
|------|------|
|
||||
| 我们设计了... | 系统采用... |
|
||||
| 开发者实现了... | 该模块通过... |
|
||||
| 代码中使用了... | 架构设计体现了... |
|
||||
|
||||
### 策略二:逻辑连接
|
||||
|
||||
使用连接词确保段落递进:
|
||||
|
||||
- **承接**:此外、进一步、在此基础上
|
||||
- **转折**:然而、值得注意的是、不同于
|
||||
- **因果**:因此、这一设计使得、由此可见
|
||||
- **总结**:综上所述、从整体来看、概言之
|
||||
|
||||
### 策略三:深度阐释
|
||||
|
||||
每个技术点需包含:
|
||||
1. **是什么**:客观描述技术实现
|
||||
2. **为什么**:阐释设计意图和考量
|
||||
3. **影响**:说明对系统的影响和价值
|
||||
|
||||
```markdown
|
||||
<!-- 示例 -->
|
||||
系统采用依赖注入模式管理组件生命周期(是什么)。这一选择源于
|
||||
对可测试性和松耦合的追求(为什么)。通过将依赖关系外置于
|
||||
配置层,各模块可独立进行单元测试,同时为运行时替换实现
|
||||
提供了可能(影响)。
|
||||
```
|
||||
|
||||
## 章节模板
|
||||
|
||||
### 架构概述(段落式)
|
||||
|
||||
```markdown
|
||||
## 系统架构概述
|
||||
|
||||
{项目名称}采用{架构模式}架构,整体设计围绕{核心理念}展开。
|
||||
从宏观视角审视,系统可划分为{N}个主要层次,各层职责明确,
|
||||
边界清晰。
|
||||
|
||||
{表现层/入口层}作为系统与外部交互的唯一入口,承担请求解析、
|
||||
参数校验、响应封装等职责。该层通过{框架/技术}实现,遵循
|
||||
{设计原则},确保接口的一致性与可维护性。
|
||||
|
||||
{业务层}是系统的核心所在,封装了全部业务逻辑。该层采用
|
||||
{模式/策略}组织代码,将复杂业务拆解为{N}个领域模块。
|
||||
值得注意的是,{关键设计决策}体现了对{质量属性}的重视。
|
||||
|
||||
{数据层}负责持久化与数据访问,通过{技术/框架}实现。
|
||||
该层与业务层通过{接口/抽象}解耦,使得数据源的替换
|
||||
不影响上层逻辑,体现了依赖倒置原则的应用。
|
||||
```
|
||||
|
||||
### 设计模式分析(段落式)
|
||||
|
||||
```markdown
|
||||
## 设计模式应用
|
||||
|
||||
代码库中可识别出{模式1}、{模式2}等设计模式的应用,
|
||||
这些模式的选择与系统的{核心需求}密切相关。
|
||||
|
||||
{模式1}主要应用于{场景/模块}。具体实现位于
|
||||
`{文件路径}`,通过{实现方式}达成{目标}。
|
||||
这一模式的引入有效解决了{问题},使得{效果}。
|
||||
|
||||
在{另一场景}中,系统采用{模式2}应对{挑战}。
|
||||
不同于{模式1}的{特点},{模式2}更侧重于{关注点}。
|
||||
从`{文件路径}`的实现可以看出,设计者通过
|
||||
{具体实现}实现了{目标}。
|
||||
|
||||
综合来看,模式的选择体现了对{原则}的遵循,
|
||||
为系统的{质量属性}提供了有力支撑。
|
||||
```
|
||||
|
||||
### 算法流程分析(段落式)
|
||||
|
||||
```markdown
|
||||
## 核心算法设计
|
||||
|
||||
{算法名称}是系统处理{业务场景}的核心逻辑,
|
||||
其实现位于`{文件路径}`。
|
||||
|
||||
从算法流程来看,整体可分为{N}个阶段。首先,
|
||||
{第一阶段描述},这一步骤的目的在于{目的}。
|
||||
随后,算法进入{第二阶段},通过{方法}实现{目标}。
|
||||
最终,{结果处理}完成整个处理流程。
|
||||
|
||||
在复杂度方面,该算法的时间复杂度为{O(x)},
|
||||
空间复杂度为{O(y)}。这一复杂度特征源于
|
||||
{原因},在{数据规模}场景下表现良好。
|
||||
|
||||
值得关注的是,{算法名称}采用了{优化策略},
|
||||
相较于朴素实现,{具体优化点}。这一设计决策
|
||||
使得{性能提升/效果}。
|
||||
```
|
||||
|
||||
## 质量检查清单
|
||||
|
||||
- [ ] 无清单罗列(禁止 `-` 或 `|` 表格作为主体内容)
|
||||
- [ ] 段落完整(每段 3-5 句,逻辑闭环)
|
||||
- [ ] 逻辑递进(有连接词串联)
|
||||
- [ ] 客观表达(无"我们"、"开发者"等主观主语)
|
||||
- [ ] 深度阐释(包含是什么/为什么/影响)
|
||||
- [ ] 代码引用(关键点附文件路径)
|
||||
@@ -1,10 +1,17 @@
|
||||
# Analysis Mode Protocol
|
||||
|
||||
## Mode Definition
|
||||
|
||||
**Mode**: `analysis` (READ-ONLY)
|
||||
**Tools**: Gemini, Qwen (default mode)
|
||||
## Prompt Structure
|
||||
|
||||
```
|
||||
PURPOSE: [development goal]
|
||||
TASK: [specific implementation task]
|
||||
MODE: [auto|write]
|
||||
CONTEXT: [file patterns]
|
||||
EXPECTED: [deliverables]
|
||||
RULES: [templates | additional constraints]
|
||||
```
|
||||
## Operation Boundaries
|
||||
|
||||
### ALLOWED Operations
|
||||
@@ -27,8 +34,8 @@
|
||||
2. **Read** and analyze CONTEXT files thoroughly
|
||||
3. **Identify** patterns, issues, and dependencies
|
||||
4. **Generate** insights and recommendations
|
||||
5. **Output** structured analysis (text response only)
|
||||
6. **Validate** EXPECTED deliverables met
|
||||
5. **Validate** EXPECTED deliverables met
|
||||
6. **Output** structured analysis (text response only)
|
||||
|
||||
## Core Requirements
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# Write Mode Protocol
|
||||
## Prompt Structure
|
||||
|
||||
## Mode Definition
|
||||
|
||||
**Mode**: `write` (FILE OPERATIONS) / `auto` (FULL OPERATIONS)
|
||||
**Tools**: Codex (auto), Gemini/Qwen (write)
|
||||
|
||||
```
|
||||
PURPOSE: [development goal]
|
||||
TASK: [specific implementation task]
|
||||
MODE: [auto|write]
|
||||
CONTEXT: [file patterns]
|
||||
EXPECTED: [deliverables]
|
||||
RULES: [templates | additional constraints]
|
||||
```
|
||||
## Operation Boundaries
|
||||
|
||||
### MODE: write
|
||||
@@ -15,12 +19,6 @@
|
||||
|
||||
**Restrictions**: Follow project conventions, cannot break existing functionality
|
||||
|
||||
### MODE: auto (Codex only)
|
||||
- All `write` mode operations
|
||||
- Run tests and builds
|
||||
- Commit code incrementally
|
||||
- Full autonomous development
|
||||
|
||||
**Constraint**: Must test every change
|
||||
|
||||
## Execution Flow
|
||||
@@ -33,16 +31,6 @@
|
||||
5. **Validate** changes
|
||||
6. **Report** file changes
|
||||
|
||||
### MODE: auto
|
||||
1. **Parse** all 6 fields
|
||||
2. **Analyze** CONTEXT files - find 3+ similar patterns
|
||||
3. **Plan** implementation following RULES
|
||||
4. **Generate** code with tests
|
||||
5. **Run** tests continuously
|
||||
6. **Commit** working code incrementally
|
||||
7. **Validate** EXPECTED deliverables
|
||||
8. **Report** results
|
||||
|
||||
## Core Requirements
|
||||
|
||||
**ALWAYS**:
|
||||
@@ -61,17 +49,6 @@
|
||||
- Break backward compatibility
|
||||
- Exceed 3 failed attempts without stopping
|
||||
|
||||
## Multi-Task Execution (Resume)
|
||||
|
||||
**First subtask**: Standard execution flow
|
||||
**Subsequent subtasks** (via `resume`):
|
||||
- Recall context from previous subtasks
|
||||
- Build on previous work
|
||||
- Maintain consistency
|
||||
- Test integration
|
||||
- Report context for next subtask
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Three-Attempt Rule**: On 3rd failure, stop and report what attempted, what failed, root cause
|
||||
|
||||
@@ -92,7 +69,7 @@
|
||||
|
||||
**If template has no format** → Use default format below
|
||||
|
||||
### Single Task Implementation
|
||||
### Task Implementation
|
||||
|
||||
```markdown
|
||||
# Implementation: [TASK Title]
|
||||
@@ -124,48 +101,6 @@
|
||||
[Recommendations if any]
|
||||
```
|
||||
|
||||
### Multi-Task (First Subtask)
|
||||
|
||||
```markdown
|
||||
# Subtask 1/N: [TASK Title]
|
||||
|
||||
## Changes
|
||||
[List of file changes]
|
||||
|
||||
## Implementation
|
||||
[Details with code references]
|
||||
|
||||
## Testing
|
||||
✅ Tests: X passing
|
||||
|
||||
## Context for Next Subtask
|
||||
- Key decisions: [established patterns]
|
||||
- Files created: [paths and purposes]
|
||||
- Integration points: [where next subtask should connect]
|
||||
```
|
||||
|
||||
### Multi-Task (Subsequent Subtasks)
|
||||
|
||||
```markdown
|
||||
# Subtask N/M: [TASK Title]
|
||||
|
||||
## Changes
|
||||
[List of file changes]
|
||||
|
||||
## Integration Notes
|
||||
✅ Compatible with previous subtask
|
||||
✅ Maintains established patterns
|
||||
|
||||
## Implementation
|
||||
[Details with code references]
|
||||
|
||||
## Testing
|
||||
✅ Tests: X passing
|
||||
|
||||
## Context for Next Subtask
|
||||
[If not final, provide context]
|
||||
```
|
||||
|
||||
### Partial Completion
|
||||
|
||||
```markdown
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Conflict Resolution Schema",
|
||||
"description": "Simplified schema for conflict detection and resolution",
|
||||
"description": "Schema for conflict detection, strategy generation, and resolution output",
|
||||
|
||||
"type": "object",
|
||||
"required": ["conflicts", "summary"],
|
||||
@@ -10,7 +10,7 @@
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "brief", "severity", "category", "strategies"],
|
||||
"required": ["id", "brief", "severity", "category", "strategies", "recommended"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
@@ -38,10 +38,41 @@
|
||||
"type": "string",
|
||||
"description": "详细冲突描述"
|
||||
},
|
||||
"clarification_questions": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "需要用户澄清的问题(可选)"
|
||||
"impact": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scope": { "type": "string", "description": "影响的模块/组件" },
|
||||
"compatibility": { "enum": ["Yes", "No", "Partial"] },
|
||||
"migration_required": { "type": "boolean" },
|
||||
"estimated_effort": { "type": "string", "description": "人天估计" }
|
||||
}
|
||||
},
|
||||
"overlap_analysis": {
|
||||
"type": "object",
|
||||
"description": "仅当 category=ModuleOverlap 时需要",
|
||||
"properties": {
|
||||
"new_module": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"scenarios": { "type": "array", "items": { "type": "string" } },
|
||||
"responsibilities": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"existing_modules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"scenarios": { "type": "array", "items": { "type": "string" } },
|
||||
"overlap_scenarios": { "type": "array", "items": { "type": "string" } },
|
||||
"responsibilities": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"strategies": {
|
||||
"type": "array",
|
||||
@@ -49,26 +80,34 @@
|
||||
"maxItems": 4,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "approach", "complexity", "risk"],
|
||||
"required": ["name", "approach", "complexity", "risk", "effort", "pros", "cons"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "策略名称(中文)"
|
||||
},
|
||||
"approach": {
|
||||
"type": "string",
|
||||
"description": "实现方法简述"
|
||||
},
|
||||
"complexity": {
|
||||
"enum": ["Low", "Medium", "High"]
|
||||
},
|
||||
"risk": {
|
||||
"enum": ["Low", "Medium", "High"]
|
||||
},
|
||||
"constraints": {
|
||||
"name": { "type": "string", "description": "策略名称(中文)" },
|
||||
"approach": { "type": "string", "description": "实现方法简述" },
|
||||
"complexity": { "enum": ["Low", "Medium", "High"] },
|
||||
"risk": { "enum": ["Low", "Medium", "High"] },
|
||||
"effort": { "type": "string", "description": "时间估计" },
|
||||
"pros": { "type": "array", "items": { "type": "string" }, "description": "优点" },
|
||||
"cons": { "type": "array", "items": { "type": "string" }, "description": "缺点" },
|
||||
"clarification_needed": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "实施此策略的约束条件(传递给 task-generate)"
|
||||
"description": "需要用户澄清的问题(尤其是 ModuleOverlap)"
|
||||
},
|
||||
"modifications": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["file", "section", "change_type", "old_content", "new_content", "rationale"],
|
||||
"properties": {
|
||||
"file": { "type": "string", "description": "相对项目根目录的完整路径" },
|
||||
"section": { "type": "string", "description": "Markdown heading 用于定位" },
|
||||
"change_type": { "enum": ["update", "add", "remove"] },
|
||||
"old_content": { "type": "string", "description": "原始内容片段(20-100字符,用于唯一匹配)" },
|
||||
"new_content": { "type": "string", "description": "修改后的内容" },
|
||||
"rationale": { "type": "string", "description": "修改理由" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,13 +116,20 @@
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "推荐策略索引(0-based)"
|
||||
},
|
||||
"modification_suggestions": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 5,
|
||||
"items": { "type": "string" },
|
||||
"description": "自定义处理建议(2-5条,中文)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": {
|
||||
"type": "object",
|
||||
"required": ["total"],
|
||||
"required": ["total", "critical", "high", "medium"],
|
||||
"properties": {
|
||||
"total": { "type": "integer" },
|
||||
"critical": { "type": "integer" },
|
||||
@@ -93,45 +139,13 @@
|
||||
}
|
||||
},
|
||||
|
||||
"examples": [
|
||||
{
|
||||
"conflicts": [
|
||||
{
|
||||
"id": "CON-001",
|
||||
"brief": "新认证模块与现有 AuthManager 功能重叠",
|
||||
"severity": "High",
|
||||
"category": "ModuleOverlap",
|
||||
"affected_files": ["src/auth/AuthManager.ts"],
|
||||
"description": "计划新增的 UserAuthService 与现有 AuthManager 在登录和 Token 验证场景存在重叠",
|
||||
"clarification_questions": [
|
||||
"新模块的核心职责边界是什么?",
|
||||
"哪些场景应该由新模块独立处理?"
|
||||
],
|
||||
"strategies": [
|
||||
{
|
||||
"name": "扩展现有模块",
|
||||
"approach": "在 AuthManager 中添加新功能",
|
||||
"complexity": "Low",
|
||||
"risk": "Low",
|
||||
"constraints": ["保持 AuthManager 作为唯一认证入口", "新增 MFA 方法"]
|
||||
},
|
||||
{
|
||||
"name": "职责拆分",
|
||||
"approach": "AuthManager 负责基础认证,新模块负责高级认证",
|
||||
"complexity": "Medium",
|
||||
"risk": "Medium",
|
||||
"constraints": ["定义清晰的接口边界", "基础认证 = 密码+token", "高级认证 = MFA+OAuth"]
|
||||
}
|
||||
],
|
||||
"recommended": 0
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 1,
|
||||
"critical": 0,
|
||||
"high": 1,
|
||||
"medium": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
"_quality_standards": {
|
||||
"modifications": [
|
||||
"old_content: 20-100字符,确保 Edit 工具能唯一匹配",
|
||||
"new_content: 保持 markdown 格式",
|
||||
"change_type: update(替换), add(插入), remove(删除)"
|
||||
],
|
||||
"user_facing_text": "brief, name, pros, cons, modification_suggestions 使用中文",
|
||||
"technical_fields": "severity, category, complexity, risk 使用英文"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +65,13 @@ RULES: $(cat ~/.claude/workflows/cli-templates/protocols/[mode]-protocol.md) $(c
|
||||
ccw cli -p "<PROMPT>" --tool <gemini|qwen|codex> --mode <analysis|write>
|
||||
```
|
||||
|
||||
**⚠️ CRITICAL**: `--mode` parameter is **MANDATORY** for all CLI executions. No defaults are assumed.
|
||||
**Note**: `--mode` defaults to `analysis` if not specified. Explicitly specify `--mode write` for file operations.
|
||||
|
||||
### Core Principles
|
||||
|
||||
- **Use tools early and often** - Tools are faster and more thorough
|
||||
- **Unified CLI** - Always use `ccw cli -p` for consistent parameter handling
|
||||
- **Mode is MANDATORY** - ALWAYS explicitly specify `--mode analysis|write` (no implicit defaults)
|
||||
- **Default mode is analysis** - Omit `--mode` for read-only operations, explicitly use `--mode write` for file modifications
|
||||
- **One template required** - ALWAYS reference exactly ONE template in RULES (use universal fallback if no specific match)
|
||||
- **Write protection** - Require EXPLICIT `--mode write` for file operations
|
||||
- **Use double quotes for shell expansion** - Always wrap prompts in double quotes `"..."` to enable `$(cat ...)` command substitution; NEVER use single quotes or escape characters (`\$`, `\"`, `\'`)
|
||||
@@ -183,7 +183,6 @@ ASSISTANT RESPONSE: [Previous output]
|
||||
|
||||
**Tool Behavior**: Codex uses native `codex resume`; Gemini/Qwen assembles context as single prompt
|
||||
|
||||
---
|
||||
|
||||
## Prompt Template
|
||||
|
||||
@@ -362,10 +361,6 @@ ccw cli -p "RULES: \$(cat ~/.claude/workflows/cli-templates/protocols/analysis-p
|
||||
- Description: Additional directories (comma-separated)
|
||||
- Default: none
|
||||
|
||||
- **`--timeout <ms>`**
|
||||
- Description: Timeout in milliseconds
|
||||
- Default: 300000
|
||||
|
||||
- **`--resume [id]`**
|
||||
- Description: Resume previous session
|
||||
- Default: -
|
||||
@@ -430,7 +425,7 @@ MODE: analysis
|
||||
CONTEXT: @src/auth/**/* @src/middleware/auth.ts | Memory: Using bcrypt for passwords, JWT for sessions
|
||||
EXPECTED: Security report with: severity matrix, file:line references, CVE mappings where applicable, remediation code snippets prioritized by risk
|
||||
RULES: $(cat ~/.claude/workflows/cli-templates/protocols/analysis-protocol.md) $(cat ~/.claude/workflows/cli-templates/prompts/analysis/03-assess-security-risks.txt) | Focus on authentication | Ignore test files
|
||||
" --tool gemini --cd src/auth --timeout 600000
|
||||
" --tool gemini --mode analysis --cd src/auth
|
||||
```
|
||||
|
||||
**Implementation Task** (New Feature):
|
||||
@@ -442,7 +437,7 @@ MODE: write
|
||||
CONTEXT: @src/middleware/**/* @src/config/**/* | Memory: Using Express.js, Redis already configured, existing middleware pattern in auth.ts
|
||||
EXPECTED: Production-ready code with: TypeScript types, unit tests, integration test, configuration example, migration guide
|
||||
RULES: $(cat ~/.claude/workflows/cli-templates/protocols/write-protocol.md) $(cat ~/.claude/workflows/cli-templates/prompts/development/02-implement-feature.txt) | Follow existing middleware patterns | No breaking changes
|
||||
" --tool codex --mode write --timeout 1800000
|
||||
" --tool codex --mode write
|
||||
```
|
||||
|
||||
**Bug Fix Task**:
|
||||
@@ -454,7 +449,7 @@ MODE: analysis
|
||||
CONTEXT: @src/websocket/**/* @src/services/connection-manager.ts | Memory: Using ws library, ~5000 concurrent connections in production
|
||||
EXPECTED: Root cause analysis with: memory profile, leak source (file:line), fix recommendation with code, verification steps
|
||||
RULES: $(cat ~/.claude/workflows/cli-templates/protocols/analysis-protocol.md) $(cat ~/.claude/workflows/cli-templates/prompts/analysis/01-diagnose-bug-root-cause.txt) | Focus on resource cleanup
|
||||
" --tool gemini --cd src --timeout 900000
|
||||
" --tool gemini --mode analysis --cd src
|
||||
```
|
||||
|
||||
**Refactoring Task**:
|
||||
@@ -466,30 +461,25 @@ MODE: write
|
||||
CONTEXT: @src/payments/**/* @src/types/payment.ts | Memory: Currently only Stripe, adding PayPal next sprint, must support future gateways
|
||||
EXPECTED: Refactored code with: strategy interface, concrete implementations, factory class, updated tests, migration checklist
|
||||
RULES: $(cat ~/.claude/workflows/cli-templates/protocols/write-protocol.md) $(cat ~/.claude/workflows/cli-templates/prompts/development/02-refactor-codebase.txt) | Preserve all existing behavior | Tests must pass
|
||||
" --tool gemini --mode write --timeout 1200000
|
||||
" --tool gemini --mode write
|
||||
```
|
||||
---
|
||||
|
||||
## Configuration
|
||||
## ⚙️ Execution Configuration
|
||||
|
||||
### Timeout Allocation
|
||||
### Dynamic Timeout Allocation
|
||||
|
||||
**Minimum**: 5 minutes (300000ms)
|
||||
**Minimum timeout: 5 minutes (300000ms)** - Never set below this threshold.
|
||||
|
||||
- **Simple**: 5-10min (300000-600000ms)
|
||||
- Examples: Analysis, search
|
||||
**Timeout Ranges**:
|
||||
- **Simple** (analysis, search): 5-10min (300000-600000ms)
|
||||
- **Medium** (refactoring, documentation): 10-20min (600000-1200000ms)
|
||||
- **Complex** (implementation, migration): 20-60min (1200000-3600000ms)
|
||||
- **Heavy** (large codebase, multi-file): 60-120min (3600000-7200000ms)
|
||||
|
||||
- **Medium**: 10-20min (600000-1200000ms)
|
||||
- Examples: Refactoring, documentation
|
||||
|
||||
- **Complex**: 20-60min (1200000-3600000ms)
|
||||
- Examples: Implementation, migration
|
||||
|
||||
- **Heavy**: 60-120min (3600000-7200000ms)
|
||||
- Examples: Large codebase, multi-file
|
||||
|
||||
**Codex Multiplier**: 3x allocated time (minimum 15min / 900000ms)
|
||||
**Codex Multiplier**: 3x of allocated time (minimum 15min / 900000ms)
|
||||
|
||||
**Auto-detection**: Analyze PURPOSE and TASK fields to determine timeout
|
||||
|
||||
### Permission Framework
|
||||
|
||||
@@ -523,4 +513,3 @@ RULES: $(cat ~/.claude/workflows/cli-templates/protocols/write-protocol.md) $(ca
|
||||
- [ ] **Tool selected** - `--tool gemini|qwen|codex`
|
||||
- [ ] **Template applied (REQUIRED)** - Use specific or universal fallback template
|
||||
- [ ] **Constraints specified** - Scope, requirements
|
||||
- [ ] **Timeout configured** - Based on complexity
|
||||
|
||||
105
.claude/workflows/context-tools-ace.md
Normal file
105
.claude/workflows/context-tools-ace.md
Normal file
@@ -0,0 +1,105 @@
|
||||
## MCP Tools Usage
|
||||
|
||||
### search_context (ACE) - Code Search (REQUIRED - HIGHEST PRIORITY)
|
||||
|
||||
**OVERRIDES**: All other search/discovery rules in other workflow files
|
||||
|
||||
**When**: ANY code discovery task, including:
|
||||
- Find code, understand codebase structure, locate implementations
|
||||
- Explore unknown locations
|
||||
- Verify file existence before reading
|
||||
- Pattern-based file discovery
|
||||
- Semantic code understanding
|
||||
|
||||
**Priority Rule**:
|
||||
1. **Always use mcp__ace-tool__search_context FIRST** for any code/file discovery
|
||||
2. Only use Built-in Grep for single-file exact line search (after location confirmed)
|
||||
3. Only use Built-in Read for known, confirmed file paths
|
||||
|
||||
**How**:
|
||||
```javascript
|
||||
// Natural language code search - best for understanding and exploration
|
||||
mcp__ace-tool__search_context({
|
||||
project_root_path: "/path/to/project",
|
||||
query: "authentication logic"
|
||||
})
|
||||
|
||||
// With keywords for better semantic matching
|
||||
mcp__ace-tool__search_context({
|
||||
project_root_path: "/path/to/project",
|
||||
query: "I want to find where the server handles user login. Keywords: auth, login, session"
|
||||
})
|
||||
```
|
||||
|
||||
**Good Query Examples**:
|
||||
- "Where is the function that handles user authentication?"
|
||||
- "What tests are there for the login functionality?"
|
||||
- "How is the database connected to the application?"
|
||||
- "I want to find where the server handles chunk merging. Keywords: upload chunk merge"
|
||||
- "Locate where the system refreshes cached data. Keywords: cache refresh, invalidation"
|
||||
|
||||
**Bad Query Examples** (use grep or file view instead):
|
||||
- "Find definition of constructor of class Foo" (use grep tool instead)
|
||||
- "Find all references to function bar" (use grep tool instead)
|
||||
- "Show me how Checkout class is used in services/payment.py" (use file view tool instead)
|
||||
|
||||
**Key Features**:
|
||||
- Real-time index of the codebase (always up-to-date)
|
||||
- Cross-language retrieval support
|
||||
- Semantic search with embeddings
|
||||
- No manual index initialization required
|
||||
|
||||
---
|
||||
|
||||
### read_file - Read File Contents
|
||||
|
||||
**When**: Read files found by search_context
|
||||
|
||||
**How**:
|
||||
```javascript
|
||||
read_file(path="/path/to/file.ts") // Single file
|
||||
read_file(path="/src/**/*.config.ts") // Pattern matching
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### edit_file - Modify Files
|
||||
|
||||
**When**: Built-in Edit tool fails or need advanced features
|
||||
|
||||
**How**:
|
||||
```javascript
|
||||
edit_file(path="/file.ts", old_string="...", new_string="...", mode="update")
|
||||
edit_file(path="/file.ts", line=10, content="...", mode="insert_after")
|
||||
```
|
||||
|
||||
**Modes**: `update` (replace text), `insert_after`, `insert_before`, `delete_line`
|
||||
|
||||
---
|
||||
|
||||
### write_file - Create/Overwrite Files
|
||||
|
||||
**When**: Create new files or completely replace content
|
||||
|
||||
**How**:
|
||||
```javascript
|
||||
write_file(path="/new-file.ts", content="...")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Exa - External Search
|
||||
|
||||
**When**: Find documentation/examples outside codebase
|
||||
|
||||
**How**:
|
||||
```javascript
|
||||
mcp__exa__search(query="React hooks 2025 documentation")
|
||||
mcp__exa__search(query="FastAPI auth example", numResults=10)
|
||||
mcp__exa__search(query="latest API docs", livecrawl="always")
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `query` (required): Search query string
|
||||
- `numResults` (optional): Number of results to return (default: 5)
|
||||
- `livecrawl` (optional): `"always"` or `"fallback"` for live crawling
|
||||
@@ -21,8 +21,11 @@
|
||||
- Graceful degradation
|
||||
- Don't expose sensitive info
|
||||
|
||||
|
||||
|
||||
## Core Principles
|
||||
|
||||
|
||||
**Incremental Progress**:
|
||||
- Small, testable changes
|
||||
- Commit working code frequently
|
||||
@@ -43,11 +46,58 @@
|
||||
- Maintain established patterns
|
||||
- Test integration between subtasks
|
||||
|
||||
|
||||
## System Optimization
|
||||
|
||||
**Direct Binary Calls**: Always call binaries directly in `functions.shell`, set `workdir`, avoid shell wrappers (`bash -lc`, `cmd /c`, etc.)
|
||||
|
||||
**Text Editing Priority**:
|
||||
1. Use `apply_patch` tool for all routine text edits
|
||||
2. Fall back to `sed` for single-line substitutions if unavailable
|
||||
3. Avoid Python editing scripts unless both fail
|
||||
|
||||
**apply_patch invocation**:
|
||||
```json
|
||||
{
|
||||
"command": ["apply_patch", "*** Begin Patch\n*** Update File: path/to/file\n@@\n- old\n+ new\n*** End Patch\n"],
|
||||
"workdir": "<workdir>",
|
||||
"justification": "Brief reason"
|
||||
}
|
||||
```
|
||||
|
||||
**Windows UTF-8 Encoding** (before commands):
|
||||
```powershell
|
||||
[Console]::InputEncoding = [Text.UTF8Encoding]::new($false)
|
||||
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
|
||||
chcp 65001 > $null
|
||||
```
|
||||
|
||||
## Context Acquisition (MCP Tools Priority)
|
||||
|
||||
**For task context gathering and analysis, ALWAYS prefer MCP tools**:
|
||||
|
||||
1. **smart_search** - First choice for code discovery
|
||||
- Use `smart_search(query="...")` for semantic/keyword search
|
||||
- Use `smart_search(action="find_files", pattern="*.ts")` for file discovery
|
||||
- Supports modes: `auto`, `hybrid`, `exact`, `ripgrep`
|
||||
|
||||
2. **read_file** - Batch file reading
|
||||
- Read multiple files in parallel: `read_file(path="file1.ts")`, `read_file(path="file2.ts")`
|
||||
- Supports glob patterns: `read_file(path="src/**/*.config.ts")`
|
||||
|
||||
**Priority Order**:
|
||||
```
|
||||
smart_search (discovery) → read_file (batch read) → shell commands (fallback)
|
||||
```
|
||||
|
||||
**NEVER** use shell commands (`cat`, `find`, `grep`) when MCP tools are available.
|
||||
|
||||
## Execution Checklist
|
||||
|
||||
**Before**:
|
||||
- [ ] Understand PURPOSE and TASK clearly
|
||||
- [ ] Review CONTEXT files, find 3+ patterns
|
||||
- [ ] Use smart_search to discover relevant files
|
||||
- [ ] Use read_file to batch read context files, find 3+ patterns
|
||||
- [ ] Check RULES templates and constraints
|
||||
|
||||
**During**:
|
||||
|
||||
Binary file not shown.
378
.codex/prompts/compact.md
Normal file
378
.codex/prompts/compact.md
Normal file
@@ -0,0 +1,378 @@
|
||||
---
|
||||
description: Compact current session memory into structured text for session recovery
|
||||
argument-hint: "[optional: session description]"
|
||||
---
|
||||
|
||||
# Memory Compact Command (/memory:compact)
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The `memory:compact` command **compresses current session working memory** into structured text optimized for **session recovery**, extracts critical information, and saves it to persistent storage via MCP `core_memory` tool.
|
||||
|
||||
**Core Philosophy**:
|
||||
- **Session Recovery First**: Capture everything needed to resume work seamlessly
|
||||
- **Minimize Re-exploration**: Include file paths, decisions, and state to avoid redundant analysis
|
||||
- **Preserve Train of Thought**: Keep notes and hypotheses for complex debugging
|
||||
- **Actionable State**: Record last action result and known issues
|
||||
|
||||
## 2. Parameters
|
||||
|
||||
- `"session description"` (Optional): Session description to supplement objective
|
||||
- Example: "completed core-memory module"
|
||||
- Example: "debugging JWT refresh - suspected memory leak"
|
||||
|
||||
## 3. Structured Output Format
|
||||
|
||||
```markdown
|
||||
## Session ID
|
||||
[WFS-ID if workflow session active, otherwise (none)]
|
||||
|
||||
## Project Root
|
||||
[Absolute path to project root, e.g., D:\Claude_dms3]
|
||||
|
||||
## Objective
|
||||
[High-level goal - the "North Star" of this session]
|
||||
|
||||
## Execution Plan
|
||||
[CRITICAL: Embed the LATEST plan in its COMPLETE and DETAILED form]
|
||||
|
||||
### Source: [workflow | todo | user-stated | inferred]
|
||||
|
||||
<details>
|
||||
<summary>Full Execution Plan (Click to expand)</summary>
|
||||
|
||||
[PRESERVE COMPLETE PLAN VERBATIM - DO NOT SUMMARIZE]
|
||||
- ALL phases, tasks, subtasks
|
||||
- ALL file paths (absolute)
|
||||
- ALL dependencies and prerequisites
|
||||
- ALL acceptance criteria
|
||||
- ALL status markers ([x] done, [ ] pending)
|
||||
- ALL notes and context
|
||||
|
||||
Example:
|
||||
## Phase 1: Setup
|
||||
- [x] Initialize project structure
|
||||
- Created D:\Claude_dms3\src\core\index.ts
|
||||
- Added dependencies: lodash, zod
|
||||
- [ ] Configure TypeScript
|
||||
- Update tsconfig.json for strict mode
|
||||
|
||||
## Phase 2: Implementation
|
||||
- [ ] Implement core API
|
||||
- Target: D:\Claude_dms3\src\api\handler.ts
|
||||
- Dependencies: Phase 1 complete
|
||||
- Acceptance: All tests pass
|
||||
|
||||
</details>
|
||||
|
||||
## Working Files (Modified)
|
||||
[Absolute paths to actively modified files]
|
||||
- D:\Claude_dms3\src\file1.ts (role: main implementation)
|
||||
- D:\Claude_dms3\tests\file1.test.ts (role: unit tests)
|
||||
|
||||
## Reference Files (Read-Only)
|
||||
[Absolute paths to context files - NOT modified but essential for understanding]
|
||||
- D:\Claude_dms3\.claude\CLAUDE.md (role: project instructions)
|
||||
- D:\Claude_dms3\src\types\index.ts (role: type definitions)
|
||||
- D:\Claude_dms3\package.json (role: dependencies)
|
||||
|
||||
## Last Action
|
||||
[Last significant action and its result/status]
|
||||
|
||||
## Decisions
|
||||
- [Decision]: [Reasoning]
|
||||
- [Decision]: [Reasoning]
|
||||
|
||||
## Constraints
|
||||
- [User-specified limitation or preference]
|
||||
|
||||
## Dependencies
|
||||
- [Added/changed packages or environment requirements]
|
||||
|
||||
## Known Issues
|
||||
- [Deferred bug or edge case]
|
||||
|
||||
## Changes Made
|
||||
- [Completed modification]
|
||||
|
||||
## Pending
|
||||
- [Next step] or (none)
|
||||
|
||||
## Notes
|
||||
[Unstructured thoughts, hypotheses, debugging trails]
|
||||
```
|
||||
|
||||
## 4. Field Definitions
|
||||
|
||||
| Field | Purpose | Recovery Value |
|
||||
|-------|---------|----------------|
|
||||
| **Session ID** | Workflow session identifier (WFS-*) | Links memory to specific stateful task execution |
|
||||
| **Project Root** | Absolute path to project directory | Enables correct path resolution in new sessions |
|
||||
| **Objective** | Ultimate goal of the session | Prevents losing track of broader feature |
|
||||
| **Execution Plan** | Complete plan from any source (verbatim) | Preserves full planning context, avoids re-planning |
|
||||
| **Working Files** | Actively modified files (absolute paths) | Immediately identifies where work was happening |
|
||||
| **Reference Files** | Read-only context files (absolute paths) | Eliminates re-exploration for critical context |
|
||||
| **Last Action** | Final tool output/status | Immediate state awareness (success/failure) |
|
||||
| **Decisions** | Architectural choices + reasoning | Prevents re-litigating settled decisions |
|
||||
| **Constraints** | User-imposed limitations | Maintains personalized coding style |
|
||||
| **Dependencies** | Package/environment changes | Prevents missing dependency errors |
|
||||
| **Known Issues** | Deferred bugs/edge cases | Ensures issues aren't forgotten |
|
||||
| **Changes Made** | Completed modifications | Clear record of what was done |
|
||||
| **Pending** | Next steps | Immediate action items |
|
||||
| **Notes** | Hypotheses, debugging trails | Preserves "train of thought" |
|
||||
|
||||
## 5. Execution Flow
|
||||
|
||||
### Step 1: Analyze Current Session
|
||||
|
||||
Extract the following from conversation history:
|
||||
|
||||
```javascript
|
||||
const sessionAnalysis = {
|
||||
sessionId: "", // WFS-* if workflow session active, null otherwise
|
||||
projectRoot: "", // Absolute path: D:\Claude_dms3
|
||||
objective: "", // High-level goal (1-2 sentences)
|
||||
executionPlan: {
|
||||
source: "workflow" | "todo" | "user-stated" | "inferred",
|
||||
content: "" // Full plan content - ALWAYS preserve COMPLETE and DETAILED form
|
||||
},
|
||||
workingFiles: [], // {absolutePath, role} - modified files
|
||||
referenceFiles: [], // {absolutePath, role} - read-only context files
|
||||
lastAction: "", // Last significant action + result
|
||||
decisions: [], // {decision, reasoning}
|
||||
constraints: [], // User-specified limitations
|
||||
dependencies: [], // Added/changed packages
|
||||
knownIssues: [], // Deferred bugs
|
||||
changesMade: [], // Completed modifications
|
||||
pending: [], // Next steps
|
||||
notes: "" // Unstructured thoughts
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Generate Structured Text
|
||||
|
||||
```javascript
|
||||
// Helper: Generate execution plan section
|
||||
const generateExecutionPlan = (plan) => {
|
||||
const sourceLabels = {
|
||||
'workflow': 'workflow (IMPL_PLAN.md)',
|
||||
'todo': 'todo (TodoWrite)',
|
||||
'user-stated': 'user-stated',
|
||||
'inferred': 'inferred'
|
||||
};
|
||||
|
||||
// CRITICAL: Preserve complete plan content verbatim - DO NOT summarize
|
||||
return `### Source: ${sourceLabels[plan.source] || plan.source}
|
||||
|
||||
<details>
|
||||
<summary>Full Execution Plan (Click to expand)</summary>
|
||||
|
||||
${plan.content}
|
||||
|
||||
</details>`;
|
||||
};
|
||||
|
||||
const structuredText = `## Session ID
|
||||
${sessionAnalysis.sessionId || '(none)'}
|
||||
|
||||
## Project Root
|
||||
${sessionAnalysis.projectRoot}
|
||||
|
||||
## Objective
|
||||
${sessionAnalysis.objective}
|
||||
|
||||
## Execution Plan
|
||||
${generateExecutionPlan(sessionAnalysis.executionPlan)}
|
||||
|
||||
## Working Files (Modified)
|
||||
${sessionAnalysis.workingFiles.map(f => `- ${f.absolutePath} (role: ${f.role})`).join('\n') || '(none)'}
|
||||
|
||||
## Reference Files (Read-Only)
|
||||
${sessionAnalysis.referenceFiles.map(f => `- ${f.absolutePath} (role: ${f.role})`).join('\n') || '(none)'}
|
||||
|
||||
## Last Action
|
||||
${sessionAnalysis.lastAction}
|
||||
|
||||
## Decisions
|
||||
${sessionAnalysis.decisions.map(d => `- ${d.decision}: ${d.reasoning}`).join('\n') || '(none)'}
|
||||
|
||||
## Constraints
|
||||
${sessionAnalysis.constraints.map(c => `- ${c}`).join('\n') || '(none)'}
|
||||
|
||||
## Dependencies
|
||||
${sessionAnalysis.dependencies.map(d => `- ${d}`).join('\n') || '(none)'}
|
||||
|
||||
## Known Issues
|
||||
${sessionAnalysis.knownIssues.map(i => `- ${i}`).join('\n') || '(none)'}
|
||||
|
||||
## Changes Made
|
||||
${sessionAnalysis.changesMade.map(c => `- ${c}`).join('\n') || '(none)'}
|
||||
|
||||
## Pending
|
||||
${sessionAnalysis.pending.length > 0
|
||||
? sessionAnalysis.pending.map(p => `- ${p}`).join('\n')
|
||||
: '(none)'}
|
||||
|
||||
## Notes
|
||||
${sessionAnalysis.notes || '(none)'}`
|
||||
```
|
||||
|
||||
### Step 3: Import to Core Memory via MCP
|
||||
|
||||
Use the MCP `core_memory` tool to save the structured text:
|
||||
|
||||
```javascript
|
||||
mcp__ccw-tools__core_memory({
|
||||
operation: "import",
|
||||
text: structuredText
|
||||
})
|
||||
```
|
||||
|
||||
Or via CLI (pipe structured text to import):
|
||||
|
||||
```bash
|
||||
# Write structured text to temp file, then import
|
||||
echo "$structuredText" | ccw core-memory import
|
||||
|
||||
# Or from a file
|
||||
ccw core-memory import --file /path/to/session-memory.md
|
||||
```
|
||||
|
||||
**Response Format**:
|
||||
```json
|
||||
{
|
||||
"operation": "import",
|
||||
"id": "CMEM-YYYYMMDD-HHMMSS",
|
||||
"message": "Created memory: CMEM-YYYYMMDD-HHMMSS"
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Report Recovery ID
|
||||
|
||||
After successful import, **clearly display the Recovery ID** to the user:
|
||||
|
||||
```
|
||||
╔════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ✓ Session Memory Saved ║
|
||||
║ ║
|
||||
║ Recovery ID: CMEM-YYYYMMDD-HHMMSS ║
|
||||
║ ║
|
||||
║ To restore: "Please import memory <ID>" ║
|
||||
║ (MCP: core_memory export | CLI: ccw core-memory export --id <ID>) ║
|
||||
╚════════════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
## 6. Quality Checklist
|
||||
|
||||
Before generating:
|
||||
- [ ] Session ID captured if workflow session active (WFS-*)
|
||||
- [ ] Project Root is absolute path (e.g., D:\Claude_dms3)
|
||||
- [ ] Objective clearly states the "North Star" goal
|
||||
- [ ] Execution Plan: COMPLETE plan preserved VERBATIM (no summarization)
|
||||
- [ ] Plan Source: Clearly identified (workflow | todo | user-stated | inferred)
|
||||
- [ ] Plan Details: ALL phases, tasks, file paths, dependencies, status markers included
|
||||
- [ ] All file paths are ABSOLUTE (not relative)
|
||||
- [ ] Working Files: 3-8 modified files with roles
|
||||
- [ ] Reference Files: Key context files (CLAUDE.md, types, configs)
|
||||
- [ ] Last Action captures final state (success/failure)
|
||||
- [ ] Decisions include reasoning, not just choices
|
||||
- [ ] Known Issues separates deferred from forgotten bugs
|
||||
- [ ] Notes preserve debugging hypotheses if any
|
||||
|
||||
## 7. Path Resolution Rules
|
||||
|
||||
### Project Root Detection
|
||||
1. Check current working directory from environment
|
||||
2. Look for project markers: `.git/`, `package.json`, `.claude/`
|
||||
3. Use the topmost directory containing these markers
|
||||
|
||||
### Absolute Path Conversion
|
||||
```javascript
|
||||
// Convert relative to absolute
|
||||
const toAbsolutePath = (relativePath, projectRoot) => {
|
||||
if (path.isAbsolute(relativePath)) return relativePath;
|
||||
return path.join(projectRoot, relativePath);
|
||||
};
|
||||
|
||||
// Example: "src/api/auth.ts" → "D:\Claude_dms3\src\api\auth.ts"
|
||||
```
|
||||
|
||||
### Reference File Categories
|
||||
| Category | Examples | Priority |
|
||||
|----------|----------|----------|
|
||||
| Project Config | `.claude/CLAUDE.md`, `package.json`, `tsconfig.json` | High |
|
||||
| Type Definitions | `src/types/*.ts`, `*.d.ts` | High |
|
||||
| Related Modules | Parent/sibling modules with shared interfaces | Medium |
|
||||
| Test Files | Corresponding test files for modified code | Medium |
|
||||
| Documentation | `README.md`, `ARCHITECTURE.md` | Low |
|
||||
|
||||
## 8. Plan Detection (Priority Order)
|
||||
|
||||
### Priority 1: Workflow Session (IMPL_PLAN.md)
|
||||
```javascript
|
||||
// Check for active workflow session
|
||||
const manifest = await mcp__ccw-tools__session_manager({
|
||||
operation: "list",
|
||||
location: "active"
|
||||
});
|
||||
|
||||
if (manifest.sessions?.length > 0) {
|
||||
const session = manifest.sessions[0];
|
||||
const plan = await mcp__ccw-tools__session_manager({
|
||||
operation: "read",
|
||||
session_id: session.id,
|
||||
content_type: "plan"
|
||||
});
|
||||
sessionAnalysis.sessionId = session.id;
|
||||
sessionAnalysis.executionPlan.source = "workflow";
|
||||
sessionAnalysis.executionPlan.content = plan.content;
|
||||
}
|
||||
```
|
||||
|
||||
### Priority 2: TodoWrite (Current Session Todos)
|
||||
```javascript
|
||||
// Extract from conversation - look for TodoWrite tool calls
|
||||
// Preserve COMPLETE todo list with all details
|
||||
const todos = extractTodosFromConversation();
|
||||
if (todos.length > 0) {
|
||||
sessionAnalysis.executionPlan.source = "todo";
|
||||
// Format todos with full context - preserve status markers
|
||||
sessionAnalysis.executionPlan.content = todos.map(t =>
|
||||
`- [${t.status === 'completed' ? 'x' : t.status === 'in_progress' ? '>' : ' '}] ${t.content}`
|
||||
).join('\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Priority 3: User-Stated Plan
|
||||
```javascript
|
||||
// Look for explicit plan statements in user messages:
|
||||
// - "Here's my plan: 1. ... 2. ... 3. ..."
|
||||
// - "I want to: first..., then..., finally..."
|
||||
// - Numbered or bulleted lists describing steps
|
||||
const userPlan = extractUserStatedPlan();
|
||||
if (userPlan) {
|
||||
sessionAnalysis.executionPlan.source = "user-stated";
|
||||
sessionAnalysis.executionPlan.content = userPlan;
|
||||
}
|
||||
```
|
||||
|
||||
### Priority 4: Inferred Plan
|
||||
```javascript
|
||||
// If no explicit plan, infer from:
|
||||
// - Task description and breakdown discussion
|
||||
// - Sequence of actions taken
|
||||
// - Outstanding work mentioned
|
||||
const inferredPlan = inferPlanFromDiscussion();
|
||||
if (inferredPlan) {
|
||||
sessionAnalysis.executionPlan.source = "inferred";
|
||||
sessionAnalysis.executionPlan.content = inferredPlan;
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Notes
|
||||
|
||||
- **Timing**: Execute at task completion or before context switch
|
||||
- **Frequency**: Once per independent task or milestone
|
||||
- **Recovery**: New session can immediately continue with full context
|
||||
- **Knowledge Graph**: Entity relationships auto-extracted for visualization
|
||||
- **Absolute Paths**: Critical for cross-session recovery on different machines
|
||||
@@ -1,25 +1,62 @@
|
||||
# Gemini Code Guidelines
|
||||
|
||||
## Code Quality Standards
|
||||
|
||||
### Code Quality
|
||||
- Follow project's existing patterns
|
||||
- Match import style and naming conventions
|
||||
- Single responsibility per function/class
|
||||
- DRY (Don't Repeat Yourself)
|
||||
- YAGNI (You Aren't Gonna Need It)
|
||||
|
||||
### Testing
|
||||
- Test all public functions
|
||||
- Test edge cases and error conditions
|
||||
- Mock external dependencies
|
||||
- Target 80%+ coverage
|
||||
|
||||
### Error Handling
|
||||
- Proper try-catch blocks
|
||||
- Clear error messages
|
||||
- Graceful degradation
|
||||
- Don't expose sensitive info
|
||||
|
||||
## Core Principles
|
||||
|
||||
**Thoroughness**:
|
||||
- Analyze ALL CONTEXT files completely
|
||||
- Check cross-file patterns and dependencies
|
||||
- Identify edge cases and quantify metrics
|
||||
**Incremental Progress**:
|
||||
- Small, testable changes
|
||||
- Commit working code frequently
|
||||
- Build on previous work (subtasks)
|
||||
|
||||
**Evidence-Based**:
|
||||
- Quote relevant code with `file:line` references
|
||||
- Link related patterns across files
|
||||
- Support all claims with concrete examples
|
||||
- Study 3+ similar patterns before implementing
|
||||
- Match project style exactly
|
||||
- Verify with existing code
|
||||
|
||||
**Actionable**:
|
||||
- Clear, specific recommendations (not vague)
|
||||
- Prioritized by impact
|
||||
- Incremental changes over big rewrites
|
||||
**Pragmatic**:
|
||||
- Boring solutions over clever code
|
||||
- Simple over complex
|
||||
- Adapt to project reality
|
||||
|
||||
**Philosophy**:
|
||||
- **Simple over complex** - Avoid over-engineering
|
||||
- **Clear over clever** - Prefer obvious solutions
|
||||
- **Learn from existing** - Reference project patterns
|
||||
- **Pragmatic over dogmatic** - Adapt to project reality
|
||||
- **Incremental progress** - Small, testable changes
|
||||
**Context Continuity** (Multi-Task):
|
||||
- Leverage resume for consistency
|
||||
- Maintain established patterns
|
||||
- Test integration between subtasks
|
||||
|
||||
## Execution Checklist
|
||||
|
||||
**Before**:
|
||||
- [ ] Understand PURPOSE and TASK clearly
|
||||
- [ ] Review CONTEXT files, find 3+ patterns
|
||||
- [ ] Check RULES templates and constraints
|
||||
|
||||
**During**:
|
||||
- [ ] Follow existing patterns exactly
|
||||
- [ ] Write tests alongside code
|
||||
- [ ] Run tests after every change
|
||||
- [ ] Commit working code incrementally
|
||||
|
||||
**After**:
|
||||
- [ ] All tests pass
|
||||
- [ ] Coverage meets target
|
||||
- [ ] Build succeeds
|
||||
- [ ] All EXPECTED deliverables met
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -29,3 +29,4 @@ COMMAND_TEMPLATE_ORCHESTRATOR.md
|
||||
settings.json
|
||||
*.mcp.json
|
||||
.mcp.json
|
||||
.ace-tool/
|
||||
|
||||
19
.mcp.json
19
.mcp.json
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"chrome-devtools-mcp@latest"
|
||||
],
|
||||
"env": {}
|
||||
},
|
||||
"ccw-tools": {
|
||||
"command": "ccw-mcp",
|
||||
"args": [],
|
||||
"env": {
|
||||
"CCW_ENABLED_TOOLS": "write_file,edit_file,smart_search,core_memory"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
331
AGENTS.md
Normal file
331
AGENTS.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# Codex Agent Execution Protocol
|
||||
|
||||
## Overview
|
||||
|
||||
**Role**: Autonomous development, implementation, and testing specialist
|
||||
|
||||
|
||||
## Prompt Structure
|
||||
|
||||
All prompts follow this 6-field format:
|
||||
|
||||
```
|
||||
PURPOSE: [development goal]
|
||||
TASK: [specific implementation task]
|
||||
MODE: [auto|write]
|
||||
CONTEXT: [file patterns]
|
||||
EXPECTED: [deliverables]
|
||||
RULES: [templates | additional constraints]
|
||||
```
|
||||
|
||||
**Subtask indicator**: `Subtask N of M: [title]` or `CONTINUE TO NEXT SUBTASK`
|
||||
|
||||
## MODE Definitions
|
||||
|
||||
### MODE: auto (default)
|
||||
|
||||
**Permissions**:
|
||||
- Full file operations (create/modify/delete)
|
||||
- Run tests and builds
|
||||
- Commit code incrementally
|
||||
|
||||
**Execute**:
|
||||
1. Parse PURPOSE and TASK
|
||||
2. Analyze CONTEXT files - find 3+ similar patterns
|
||||
3. Plan implementation following RULES
|
||||
4. Generate code with tests
|
||||
5. Run tests continuously
|
||||
6. Commit working code incrementally
|
||||
7. Validate EXPECTED deliverables
|
||||
8. Report results (with context for next subtask if multi-task)
|
||||
|
||||
**Constraint**: Must test every change
|
||||
|
||||
### MODE: write
|
||||
|
||||
**Permissions**:
|
||||
- Focused file operations
|
||||
- Create/modify specific files
|
||||
- Run tests for validation
|
||||
|
||||
**Execute**:
|
||||
1. Analyze CONTEXT files
|
||||
2. Make targeted changes
|
||||
3. Validate tests pass
|
||||
4. Report file changes
|
||||
|
||||
## Execution Protocol
|
||||
|
||||
### Core Requirements
|
||||
|
||||
**ALWAYS**:
|
||||
- Parse all 6 fields (PURPOSE, TASK, MODE, CONTEXT, EXPECTED, RULES)
|
||||
- Study CONTEXT files - find 3+ similar patterns before implementing
|
||||
- Apply RULES (templates + constraints) exactly
|
||||
- Test continuously after every change
|
||||
- Commit incrementally with working code
|
||||
- Match project style and patterns exactly
|
||||
- List all created/modified files at output beginning
|
||||
- Use direct binary calls (avoid shell wrappers)
|
||||
- Prefer apply_patch for text edits
|
||||
- Configure Windows UTF-8 encoding for Chinese support
|
||||
|
||||
**NEVER**:
|
||||
- Make assumptions without code verification
|
||||
- Ignore existing patterns
|
||||
- Skip tests
|
||||
- Use clever tricks over boring solutions
|
||||
- Over-engineer solutions
|
||||
- Break existing code or backward compatibility
|
||||
- Exceed 3 failed attempts without stopping
|
||||
|
||||
### RULES Processing
|
||||
|
||||
- Parse RULES field to extract template content and constraints
|
||||
- Recognize `|` as separator: `template content | additional constraints`
|
||||
- Apply ALL template guidelines as mandatory
|
||||
- Apply ALL additional constraints as mandatory
|
||||
- Treat rule violations as task failures
|
||||
|
||||
### Multi-Task Execution (Resume Pattern)
|
||||
|
||||
**First subtask**: Standard execution flow above
|
||||
**Subsequent subtasks** (via `resume --last`):
|
||||
- Recall context from previous subtasks
|
||||
- Build on previous work (don't repeat)
|
||||
- Maintain consistency with established patterns
|
||||
- Focus on current subtask scope only
|
||||
- Test integration with previous work
|
||||
- Report context for next subtask
|
||||
|
||||
## System Optimization
|
||||
|
||||
**Direct Binary Calls**: Always call binaries directly in `functions.shell`, set `workdir`, avoid shell wrappers (`bash -lc`, `cmd /c`, etc.)
|
||||
|
||||
**Text Editing Priority**:
|
||||
1. Use `apply_patch` tool for all routine text edits
|
||||
2. Fall back to `sed` for single-line substitutions if unavailable
|
||||
3. Avoid Python editing scripts unless both fail
|
||||
|
||||
**apply_patch invocation**:
|
||||
```json
|
||||
{
|
||||
"command": ["apply_patch", "*** Begin Patch\n*** Update File: path/to/file\n@@\n- old\n+ new\n*** End Patch\n"],
|
||||
"workdir": "<workdir>",
|
||||
"justification": "Brief reason"
|
||||
}
|
||||
```
|
||||
|
||||
**Windows UTF-8 Encoding** (before commands):
|
||||
```powershell
|
||||
[Console]::InputEncoding = [Text.UTF8Encoding]::new($false)
|
||||
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
|
||||
chcp 65001 > $null
|
||||
```
|
||||
|
||||
## Output Standards
|
||||
|
||||
### Format Priority
|
||||
|
||||
**If template defines output format** → Follow template format EXACTLY (all sections mandatory)
|
||||
|
||||
**If template has no format** → Use default format below based on task type
|
||||
|
||||
### Default Output Formats
|
||||
|
||||
#### Single Task Implementation
|
||||
|
||||
```markdown
|
||||
# Implementation: [TASK Title]
|
||||
|
||||
## Changes
|
||||
- Created: `path/to/file1.ext` (X lines)
|
||||
- Modified: `path/to/file2.ext` (+Y/-Z lines)
|
||||
- Deleted: `path/to/file3.ext`
|
||||
|
||||
## Summary
|
||||
[2-3 sentence overview of what was implemented]
|
||||
|
||||
## Key Decisions
|
||||
1. [Decision] - Rationale and reference to similar pattern
|
||||
2. [Decision] - path/to/reference:line
|
||||
|
||||
## Implementation Details
|
||||
[Evidence-based description with code references]
|
||||
|
||||
## Testing
|
||||
- Tests written: X new tests
|
||||
- Tests passing: Y/Z tests
|
||||
- Coverage: N%
|
||||
|
||||
## Validation
|
||||
✅ Tests: X passing
|
||||
✅ Coverage: Y%
|
||||
✅ Build: Success
|
||||
|
||||
## Next Steps
|
||||
[Recommendations or future improvements]
|
||||
```
|
||||
|
||||
#### Multi-Task Execution (with Resume)
|
||||
|
||||
**First Subtask**:
|
||||
```markdown
|
||||
# Subtask 1/N: [TASK Title]
|
||||
|
||||
## Changes
|
||||
[List of file changes]
|
||||
|
||||
## Implementation
|
||||
[Details with code references]
|
||||
|
||||
## Testing
|
||||
✅ Tests: X passing
|
||||
✅ Integration: Compatible with existing code
|
||||
|
||||
## Context for Next Subtask
|
||||
- Key decisions: [established patterns]
|
||||
- Files created: [paths and purposes]
|
||||
- Integration points: [where next subtask should connect]
|
||||
```
|
||||
|
||||
**Subsequent Subtasks**:
|
||||
```markdown
|
||||
# Subtask N/M: [TASK Title]
|
||||
|
||||
## Changes
|
||||
[List of file changes]
|
||||
|
||||
## Integration Notes
|
||||
✅ Compatible with subtask N-1
|
||||
✅ Maintains established patterns
|
||||
✅ Tests pass with previous work
|
||||
|
||||
## Implementation
|
||||
[Details with code references]
|
||||
|
||||
## Testing
|
||||
✅ Tests: X passing
|
||||
✅ Total coverage: Y%
|
||||
|
||||
## Context for Next Subtask
|
||||
[If not final subtask, provide context for continuation]
|
||||
```
|
||||
|
||||
#### Partial Completion
|
||||
|
||||
```markdown
|
||||
# Task Status: Partially Completed
|
||||
|
||||
## Completed
|
||||
- [What worked successfully]
|
||||
- Files: `path/to/completed.ext`
|
||||
|
||||
## Blocked
|
||||
- **Issue**: [What failed]
|
||||
- **Root Cause**: [Analysis of failure]
|
||||
- **Attempted**: [Solutions tried - attempt X of 3]
|
||||
|
||||
## Required
|
||||
[What's needed to proceed]
|
||||
|
||||
## Recommendation
|
||||
[Suggested next steps or alternative approaches]
|
||||
```
|
||||
|
||||
### Code References
|
||||
|
||||
**Format**: `path/to/file:line_number`
|
||||
|
||||
**Example**: `src/auth/jwt.ts:45` - Implemented token validation following pattern from `src/auth/session.ts:78`
|
||||
|
||||
### Related Files Section
|
||||
|
||||
**Always include at output beginning** - List ALL files analyzed, created, or modified:
|
||||
|
||||
```markdown
|
||||
## Related Files
|
||||
- `path/to/file1.ext` - [Role in implementation]
|
||||
- `path/to/file2.ext` - [Reference pattern used]
|
||||
- `path/to/file3.ext` - [Modified for X reason]
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Three-Attempt Rule
|
||||
|
||||
**On 3rd failed attempt**:
|
||||
1. Stop execution
|
||||
2. Report: What attempted, what failed, root cause
|
||||
3. Request guidance or suggest alternatives
|
||||
|
||||
### Recovery Strategies
|
||||
|
||||
| Error Type | Response |
|
||||
|------------|----------|
|
||||
| **Syntax/Type** | Review errors → Fix → Re-run tests → Validate build |
|
||||
| **Runtime** | Analyze stack trace → Add error handling → Test error cases |
|
||||
| **Test Failure** | Debug in isolation → Review setup → Fix implementation/test |
|
||||
| **Build Failure** | Check messages → Fix incrementally → Validate each fix |
|
||||
|
||||
## Quality Standards
|
||||
|
||||
### Code Quality
|
||||
- Follow project's existing patterns
|
||||
- Match import style and naming conventions
|
||||
- Single responsibility per function/class
|
||||
- DRY (Don't Repeat Yourself)
|
||||
- YAGNI (You Aren't Gonna Need It)
|
||||
|
||||
### Testing
|
||||
- Test all public functions
|
||||
- Test edge cases and error conditions
|
||||
- Mock external dependencies
|
||||
- Target 80%+ coverage
|
||||
|
||||
### Error Handling
|
||||
- Proper try-catch blocks
|
||||
- Clear error messages
|
||||
- Graceful degradation
|
||||
- Don't expose sensitive info
|
||||
|
||||
## Core Principles
|
||||
|
||||
**Incremental Progress**:
|
||||
- Small, testable changes
|
||||
- Commit working code frequently
|
||||
- Build on previous work (subtasks)
|
||||
|
||||
**Evidence-Based**:
|
||||
- Study 3+ similar patterns before implementing
|
||||
- Match project style exactly
|
||||
- Verify with existing code
|
||||
|
||||
**Pragmatic**:
|
||||
- Boring solutions over clever code
|
||||
- Simple over complex
|
||||
- Adapt to project reality
|
||||
|
||||
**Context Continuity** (Multi-Task):
|
||||
- Leverage resume for consistency
|
||||
- Maintain established patterns
|
||||
- Test integration between subtasks
|
||||
|
||||
## Execution Checklist
|
||||
|
||||
**Before**:
|
||||
- [ ] Understand PURPOSE and TASK clearly
|
||||
- [ ] Review CONTEXT files, find 3+ patterns
|
||||
- [ ] Check RULES templates and constraints
|
||||
|
||||
**During**:
|
||||
- [ ] Follow existing patterns exactly
|
||||
- [ ] Write tests alongside code
|
||||
- [ ] Run tests after every change
|
||||
- [ ] Commit working code incrementally
|
||||
|
||||
**After**:
|
||||
- [ ] All tests pass
|
||||
- [ ] Coverage meets target
|
||||
- [ ] Build succeeds
|
||||
- [ ] All EXPECTED deliverables met
|
||||
196
API_SETTINGS_IMPLEMENTATION.md
Normal file
196
API_SETTINGS_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# API Settings 页面实现完成
|
||||
|
||||
## 创建的文件
|
||||
|
||||
### 1. JavaScript 文件
|
||||
**位置**: `ccw/src/templates/dashboard-js/views/api-settings.js` (28KB)
|
||||
|
||||
**主要功能**:
|
||||
- ✅ Provider Management (提供商管理)
|
||||
- 添加/编辑/删除提供商
|
||||
- 支持 OpenAI, Anthropic, Google, Ollama, Azure, Mistral, DeepSeek, Custom
|
||||
- API Key 管理(支持环境变量)
|
||||
- 连接测试功能
|
||||
|
||||
- ✅ Endpoint Management (端点管理)
|
||||
- 创建自定义端点
|
||||
- 关联提供商和模型
|
||||
- 缓存策略配置
|
||||
- 显示 CLI 使用示例
|
||||
|
||||
- ✅ Cache Management (缓存管理)
|
||||
- 全局缓存开关
|
||||
- 缓存统计显示
|
||||
- 清除缓存功能
|
||||
|
||||
### 2. CSS 样式文件
|
||||
**位置**: `ccw/src/templates/dashboard-css/31-api-settings.css` (6.8KB)
|
||||
|
||||
**样式包括**:
|
||||
- 卡片式布局
|
||||
- 表单样式
|
||||
- 进度条
|
||||
- 响应式设计
|
||||
- 空状态显示
|
||||
|
||||
### 3. 国际化支持
|
||||
**位置**: `ccw/src/templates/dashboard-js/i18n.js`
|
||||
|
||||
**添加的翻译**:
|
||||
- 英文:54 个翻译键
|
||||
- 中文:54 个翻译键
|
||||
- 包含所有 UI 文本、提示信息、错误消息
|
||||
|
||||
### 4. 配置更新
|
||||
|
||||
#### dashboard-generator.ts
|
||||
- ✅ 添加 `31-api-settings.css` 到 CSS 模块列表
|
||||
- ✅ 添加 `views/api-settings.js` 到 JS 模块列表
|
||||
|
||||
#### navigation.js
|
||||
- ✅ 添加 `api-settings` 路由处理
|
||||
- ✅ 添加标题更新逻辑
|
||||
|
||||
#### dashboard.html
|
||||
- ✅ 添加导航菜单项 (Settings 图标)
|
||||
|
||||
## API 端点使用
|
||||
|
||||
该页面使用以下后端 API(已存在):
|
||||
|
||||
### Provider APIs
|
||||
- `GET /api/litellm-api/providers` - 获取所有提供商
|
||||
- `POST /api/litellm-api/providers` - 创建提供商
|
||||
- `PUT /api/litellm-api/providers/:id` - 更新提供商
|
||||
- `DELETE /api/litellm-api/providers/:id` - 删除提供商
|
||||
- `POST /api/litellm-api/providers/:id/test` - 测试连接
|
||||
|
||||
### Endpoint APIs
|
||||
- `GET /api/litellm-api/endpoints` - 获取所有端点
|
||||
- `POST /api/litellm-api/endpoints` - 创建端点
|
||||
- `PUT /api/litellm-api/endpoints/:id` - 更新端点
|
||||
- `DELETE /api/litellm-api/endpoints/:id` - 删除端点
|
||||
|
||||
### Model Discovery
|
||||
- `GET /api/litellm-api/models/:providerType` - 获取提供商支持的模型列表
|
||||
|
||||
### Cache APIs
|
||||
- `GET /api/litellm-api/cache/stats` - 获取缓存统计
|
||||
- `POST /api/litellm-api/cache/clear` - 清除缓存
|
||||
|
||||
### Config APIs
|
||||
- `GET /api/litellm-api/config` - 获取完整配置
|
||||
- `PUT /api/litellm-api/config/cache` - 更新全局缓存设置
|
||||
|
||||
## 页面特性
|
||||
|
||||
### Provider 管理
|
||||
```
|
||||
+-- Provider Card ------------------------+
|
||||
| OpenAI Production [Edit] [Del] |
|
||||
| Type: openai |
|
||||
| Key: sk-...abc |
|
||||
| URL: https://api.openai.com/v1 |
|
||||
| Status: ✓ Enabled |
|
||||
+-----------------------------------------+
|
||||
```
|
||||
|
||||
### Endpoint 管理
|
||||
```
|
||||
+-- Endpoint Card ------------------------+
|
||||
| GPT-4o Code Review [Edit] [Del]|
|
||||
| ID: my-gpt4o |
|
||||
| Provider: OpenAI Production |
|
||||
| Model: gpt-4-turbo |
|
||||
| Cache: Enabled (60 min) |
|
||||
| Usage: ccw cli -p "..." --model my-gpt4o|
|
||||
+-----------------------------------------+
|
||||
```
|
||||
|
||||
### 表单功能
|
||||
- **Provider Form**:
|
||||
- 类型选择(8 种提供商)
|
||||
- API Key 输入(支持显示/隐藏)
|
||||
- 环境变量支持
|
||||
- Base URL 自定义
|
||||
- 启用/禁用开关
|
||||
|
||||
- **Endpoint Form**:
|
||||
- 端点 ID(CLI 使用)
|
||||
- 显示名称
|
||||
- 提供商选择(动态加载)
|
||||
- 模型选择(根据提供商动态加载)
|
||||
- 缓存策略配置
|
||||
- TTL(分钟)
|
||||
- 最大大小(KB)
|
||||
- 自动缓存文件模式
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 1. 添加提供商
|
||||
1. 点击 "Add Provider"
|
||||
2. 选择提供商类型(如 OpenAI)
|
||||
3. 输入显示名称
|
||||
4. 输入 API Key(或使用环境变量)
|
||||
5. 可选:输入自定义 API Base URL
|
||||
6. 保存
|
||||
|
||||
### 2. 创建自定义端点
|
||||
1. 点击 "Add Endpoint"
|
||||
2. 输入端点 ID(用于 CLI)
|
||||
3. 输入显示名称
|
||||
4. 选择提供商
|
||||
5. 选择模型(自动加载该提供商支持的模型)
|
||||
6. 可选:配置缓存策略
|
||||
7. 保存
|
||||
|
||||
### 3. 使用端点
|
||||
```bash
|
||||
ccw cli -p "Analyze this code..." --model my-gpt4o
|
||||
```
|
||||
|
||||
## 代码质量
|
||||
|
||||
- ✅ 遵循现有代码风格
|
||||
- ✅ 使用 i18n 函数支持国际化
|
||||
- ✅ 响应式设计(移动端友好)
|
||||
- ✅ 完整的表单验证
|
||||
- ✅ 用户友好的错误提示
|
||||
- ✅ 使用 Lucide 图标
|
||||
- ✅ 模态框复用现有样式
|
||||
- ✅ 与后端 API 完全集成
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. **基础功能测试**:
|
||||
- 添加/编辑/删除提供商
|
||||
- 添加/编辑/删除端点
|
||||
- 清除缓存
|
||||
|
||||
2. **表单验证测试**:
|
||||
- 必填字段验证
|
||||
- API Key 显示/隐藏
|
||||
- 环境变量切换
|
||||
|
||||
3. **数据加载测试**:
|
||||
- 模型列表动态加载
|
||||
- 缓存统计显示
|
||||
- 空状态显示
|
||||
|
||||
4. **国际化测试**:
|
||||
- 切换语言(英文/中文)
|
||||
- 验证所有文本正确显示
|
||||
|
||||
## 下一步
|
||||
|
||||
页面已完成并集成到项目中。启动 CCW Dashboard 后:
|
||||
1. 导航栏会显示 "API Settings" 菜单项(Settings 图标)
|
||||
2. 点击进入即可使用所有功能
|
||||
3. 所有操作会实时同步到配置文件
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 页面使用现有的 LiteLLM API 路由(`litellm-api-routes.ts`)
|
||||
- 配置保存在项目的 LiteLLM 配置文件中
|
||||
- 支持环境变量引用格式:`${VARIABLE_NAME}`
|
||||
- API Key 在显示时会自动脱敏(显示前 4 位和后 4 位)
|
||||
12
README.md
12
README.md
@@ -5,7 +5,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/catlog22/Claude-Code-Workflow/releases)
|
||||
[](https://github.com/catlog22/Claude-Code-Workflow/releases)
|
||||
[](https://www.npmjs.com/package/claude-code-workflow)
|
||||
[](LICENSE)
|
||||
[]()
|
||||
@@ -52,6 +52,16 @@ CCW is built on a set of core principles that distinguish it from traditional AI
|
||||
|
||||
## ⚙️ Installation
|
||||
|
||||
### **📋 Requirements**
|
||||
|
||||
| Platform | Node.js | Additional |
|
||||
|----------|---------|------------|
|
||||
| Windows | 20.x or 22.x LTS (recommended) | Node 23+ requires [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) |
|
||||
| macOS | 18.x+ | Xcode Command Line Tools |
|
||||
| Linux | 18.x+ | build-essential |
|
||||
|
||||
> **Note**: The `better-sqlite3` dependency requires native compilation. Using Node.js LTS versions avoids build issues.
|
||||
|
||||
### **📦 npm Install (Recommended)**
|
||||
|
||||
Install globally via npm:
|
||||
|
||||
32
README_CN.md
32
README_CN.md
@@ -1,5 +1,8 @@
|
||||
# 🚀 Claude Code Workflow (CCW)
|
||||
|
||||
[](https://smithery.ai/skills?ns=catlog22&utm_source=github&utm_medium=badge)
|
||||
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/catlog22/Claude-Code-Workflow/releases)
|
||||
@@ -13,7 +16,7 @@
|
||||
|
||||
---
|
||||
|
||||
**Claude Code Workflow (CCW)** 将 AI 开发从简单的提示词链接转变为一个强大的、上下文优先的编排系统。它通过结构化规划、确定性执行和智能多模型编排,解决了执行不确定性和误差累积的问题。
|
||||
**Claude Code Workflow (CCW)** 是一个 JSON 驱动的多智能体开发框架,具有智能 CLI 编排(Gemini/Qwen/Codex)、上下文优先架构和自动化工作流执行。它将 AI 开发从简单的提示词链接转变为一个强大的编排系统。
|
||||
|
||||
> **🎉 版本 6.2.0: 原生 CodexLens 与 Dashboard 革新**
|
||||
>
|
||||
@@ -38,8 +41,8 @@
|
||||
|
||||
CCW 构建在一系列核心原则之上,这些原则使其与传统的 AI 开发方法区别开来:
|
||||
|
||||
- **上下文优先架构**: 通过预定义的上下文收集,消除了执行过程中的不确定性,确保智能体在实现*之前*就拥有正确的信息。
|
||||
- **JSON 优先的状态管理**: 任务状态完全存储在 `.task/IMPL-*.json` 文件中,作为唯一的事实来源,实现了无需状态漂移的程序化编排。
|
||||
- **上下文优先架构**: 通过预定义的上下文收集消除执行过程中的不确定性,确保智能体在实现*之前*就拥有正确的信息。
|
||||
- **JSON 优先的状态管理**: 任务状态完全存储在 `.task/IMPL-*.json` 文件中作为唯一的事实来源,实现无状态漂移的程序化编排。
|
||||
- **自主多阶段编排**: 命令链式调用专门的子命令和智能体,以零用户干预的方式自动化复杂的工作流。
|
||||
- **多模型策略**: 充分利用不同 AI 模型(如 Gemini 用于分析,Codex 用于实现)的独特优势,以获得更优越的结果。
|
||||
- **分层内存系统**: 一个 4 层文档系统,在适当的抽象级别上提供上下文,防止信息过载。
|
||||
@@ -49,18 +52,23 @@ CCW 构建在一系列核心原则之上,这些原则使其与传统的 AI 开
|
||||
|
||||
## ⚙️ 安装
|
||||
|
||||
有关详细的安装说明,请参阅 [**INSTALL_CN.md**](INSTALL_CN.md) 指南。
|
||||
### **📦 npm 安装(推荐)**
|
||||
|
||||
### **🚀 一键快速安装**
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
Invoke-Expression (Invoke-WebRequest -Uri "https://raw.githubusercontent.com/catlog22/Claude-Code-Workflow/main/install-remote.ps1" -UseBasicParsing).Content
|
||||
通过 npm 全局安装:
|
||||
```bash
|
||||
npm install -g claude-code-workflow
|
||||
```
|
||||
|
||||
**Linux/macOS (Bash/Zsh):**
|
||||
然后将工作流文件安装到您的系统:
|
||||
```bash
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/catlog22/Claude-Code-Workflow/main/install-remote.sh)
|
||||
# 交互式安装
|
||||
ccw install
|
||||
|
||||
# 全局安装(到 ~/.claude)
|
||||
ccw install -m Global
|
||||
|
||||
# 项目特定安装
|
||||
ccw install -m Path -p /path/to/project
|
||||
```
|
||||
|
||||
### **✅ 验证安装**
|
||||
@@ -283,4 +291,4 @@ CCW 提供全面的文档,帮助您快速上手并掌握高级功能:
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
此项目根据 **MIT 许可证** 授权。详见 [LICENSE](LICENSE) 文件。
|
||||
此项目根据 **MIT 许可证** 授权。详见 [LICENSE](LICENSE) 文件。
|
||||
|
||||
180
ccw-litellm/README.md
Normal file
180
ccw-litellm/README.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# ccw-litellm
|
||||
|
||||
Unified LiteLLM interface layer shared by ccw and codex-lens projects.
|
||||
|
||||
## Features
|
||||
|
||||
- **Unified LLM Interface**: Abstract interface for LLM operations (chat, completion)
|
||||
- **Unified Embedding Interface**: Abstract interface for text embeddings
|
||||
- **Multi-Provider Support**: OpenAI, Anthropic, Azure, and more via LiteLLM
|
||||
- **Configuration Management**: YAML-based configuration with environment variable substitution
|
||||
- **Type Safety**: Full type annotations with Pydantic models
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Configuration
|
||||
|
||||
Create a configuration file at `~/.ccw/config/litellm-config.yaml`:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
default_provider: openai
|
||||
|
||||
providers:
|
||||
openai:
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
api_base: https://api.openai.com/v1
|
||||
|
||||
llm_models:
|
||||
default:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
|
||||
embedding_models:
|
||||
default:
|
||||
provider: openai
|
||||
model: text-embedding-3-small
|
||||
dimensions: 1536
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
#### LLM Client
|
||||
|
||||
```python
|
||||
from ccw_litellm import LiteLLMClient, ChatMessage
|
||||
|
||||
# Initialize client with default model
|
||||
client = LiteLLMClient(model="default")
|
||||
|
||||
# Chat completion
|
||||
messages = [
|
||||
ChatMessage(role="user", content="Hello, how are you?")
|
||||
]
|
||||
response = client.chat(messages)
|
||||
print(response.content)
|
||||
|
||||
# Text completion
|
||||
response = client.complete("Once upon a time")
|
||||
print(response.content)
|
||||
```
|
||||
|
||||
#### Embedder
|
||||
|
||||
```python
|
||||
from ccw_litellm import LiteLLMEmbedder
|
||||
|
||||
# Initialize embedder with default model
|
||||
embedder = LiteLLMEmbedder(model="default")
|
||||
|
||||
# Embed single text
|
||||
vector = embedder.embed("Hello world")
|
||||
print(vector.shape) # (1, 1536)
|
||||
|
||||
# Embed multiple texts
|
||||
vectors = embedder.embed(["Text 1", "Text 2", "Text 3"])
|
||||
print(vectors.shape) # (3, 1536)
|
||||
```
|
||||
|
||||
#### Custom Configuration
|
||||
|
||||
```python
|
||||
from ccw_litellm import LiteLLMClient, load_config
|
||||
|
||||
# Load custom configuration
|
||||
config = load_config("/path/to/custom-config.yaml")
|
||||
|
||||
# Use custom configuration
|
||||
client = LiteLLMClient(model="fast", config=config)
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### Provider Configuration
|
||||
|
||||
```yaml
|
||||
providers:
|
||||
<provider_name>:
|
||||
api_key: <api_key_or_${ENV_VAR}>
|
||||
api_base: <base_url>
|
||||
```
|
||||
|
||||
Supported providers: `openai`, `anthropic`, `azure`, `vertex_ai`, `bedrock`, etc.
|
||||
|
||||
### LLM Model Configuration
|
||||
|
||||
```yaml
|
||||
llm_models:
|
||||
<model_name>:
|
||||
provider: <provider_name>
|
||||
model: <model_identifier>
|
||||
```
|
||||
|
||||
### Embedding Model Configuration
|
||||
|
||||
```yaml
|
||||
embedding_models:
|
||||
<model_name>:
|
||||
provider: <provider_name>
|
||||
model: <model_identifier>
|
||||
dimensions: <embedding_dimensions>
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The configuration supports environment variable substitution using the `${VAR}` or `${VAR:-default}` syntax:
|
||||
|
||||
```yaml
|
||||
providers:
|
||||
openai:
|
||||
api_key: ${OPENAI_API_KEY} # Required
|
||||
api_base: ${OPENAI_API_BASE:-https://api.openai.com/v1} # With default
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Interfaces
|
||||
|
||||
- `AbstractLLMClient`: Abstract base class for LLM clients
|
||||
- `AbstractEmbedder`: Abstract base class for embedders
|
||||
- `ChatMessage`: Message data class (role, content)
|
||||
- `LLMResponse`: Response data class (content, raw)
|
||||
|
||||
### Implementations
|
||||
|
||||
- `LiteLLMClient`: LiteLLM implementation of AbstractLLMClient
|
||||
- `LiteLLMEmbedder`: LiteLLM implementation of AbstractEmbedder
|
||||
|
||||
### Configuration
|
||||
|
||||
- `LiteLLMConfig`: Root configuration model
|
||||
- `ProviderConfig`: Provider configuration model
|
||||
- `LLMModelConfig`: LLM model configuration model
|
||||
- `EmbeddingModelConfig`: Embedding model configuration model
|
||||
- `load_config(path)`: Load configuration from YAML file
|
||||
- `get_config(path, reload)`: Get global configuration singleton
|
||||
- `reset_config()`: Reset global configuration (for testing)
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
pytest tests/ -v
|
||||
```
|
||||
|
||||
### Type Checking
|
||||
|
||||
```bash
|
||||
mypy src/ccw_litellm
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
53
ccw-litellm/litellm-config.yaml.example
Normal file
53
ccw-litellm/litellm-config.yaml.example
Normal file
@@ -0,0 +1,53 @@
|
||||
# LiteLLM Unified Configuration
|
||||
# Copy to ~/.ccw/config/litellm-config.yaml
|
||||
|
||||
version: 1
|
||||
|
||||
# Default provider for LLM calls
|
||||
default_provider: openai
|
||||
|
||||
# Provider configurations
|
||||
providers:
|
||||
openai:
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
api_base: https://api.openai.com/v1
|
||||
|
||||
anthropic:
|
||||
api_key: ${ANTHROPIC_API_KEY}
|
||||
|
||||
ollama:
|
||||
api_base: http://localhost:11434
|
||||
|
||||
azure:
|
||||
api_key: ${AZURE_API_KEY}
|
||||
api_base: ${AZURE_API_BASE}
|
||||
|
||||
# LLM model configurations
|
||||
llm_models:
|
||||
default:
|
||||
provider: openai
|
||||
model: gpt-4o
|
||||
fast:
|
||||
provider: openai
|
||||
model: gpt-4o-mini
|
||||
claude:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4-20250514
|
||||
local:
|
||||
provider: ollama
|
||||
model: llama3.2
|
||||
|
||||
# Embedding model configurations
|
||||
embedding_models:
|
||||
default:
|
||||
provider: openai
|
||||
model: text-embedding-3-small
|
||||
dimensions: 1536
|
||||
large:
|
||||
provider: openai
|
||||
model: text-embedding-3-large
|
||||
dimensions: 3072
|
||||
ada:
|
||||
provider: openai
|
||||
model: text-embedding-ada-002
|
||||
dimensions: 1536
|
||||
35
ccw-litellm/pyproject.toml
Normal file
35
ccw-litellm/pyproject.toml
Normal file
@@ -0,0 +1,35 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "ccw-litellm"
|
||||
version = "0.1.0"
|
||||
description = "Unified LiteLLM interface layer shared by ccw and codex-lens"
|
||||
requires-python = ">=3.10"
|
||||
authors = [{ name = "ccw-litellm contributors" }]
|
||||
dependencies = [
|
||||
"litellm>=1.0.0",
|
||||
"pyyaml",
|
||||
"numpy",
|
||||
"pydantic>=2.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
ccw-litellm = "ccw_litellm.cli:main"
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = { "" = "src" }
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
include = ["ccw_litellm*"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = "-q"
|
||||
12
ccw-litellm/src/ccw_litellm.egg-info/PKG-INFO
Normal file
12
ccw-litellm/src/ccw_litellm.egg-info/PKG-INFO
Normal file
@@ -0,0 +1,12 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: ccw-litellm
|
||||
Version: 0.1.0
|
||||
Summary: Unified LiteLLM interface layer shared by ccw and codex-lens
|
||||
Author: ccw-litellm contributors
|
||||
Requires-Python: >=3.10
|
||||
Requires-Dist: litellm>=1.0.0
|
||||
Requires-Dist: pyyaml
|
||||
Requires-Dist: numpy
|
||||
Requires-Dist: pydantic>=2.0
|
||||
Provides-Extra: dev
|
||||
Requires-Dist: pytest>=7.0; extra == "dev"
|
||||
20
ccw-litellm/src/ccw_litellm.egg-info/SOURCES.txt
Normal file
20
ccw-litellm/src/ccw_litellm.egg-info/SOURCES.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
README.md
|
||||
pyproject.toml
|
||||
src/ccw_litellm/__init__.py
|
||||
src/ccw_litellm/cli.py
|
||||
src/ccw_litellm.egg-info/PKG-INFO
|
||||
src/ccw_litellm.egg-info/SOURCES.txt
|
||||
src/ccw_litellm.egg-info/dependency_links.txt
|
||||
src/ccw_litellm.egg-info/entry_points.txt
|
||||
src/ccw_litellm.egg-info/requires.txt
|
||||
src/ccw_litellm.egg-info/top_level.txt
|
||||
src/ccw_litellm/clients/__init__.py
|
||||
src/ccw_litellm/clients/litellm_embedder.py
|
||||
src/ccw_litellm/clients/litellm_llm.py
|
||||
src/ccw_litellm/config/__init__.py
|
||||
src/ccw_litellm/config/loader.py
|
||||
src/ccw_litellm/config/models.py
|
||||
src/ccw_litellm/interfaces/__init__.py
|
||||
src/ccw_litellm/interfaces/embedder.py
|
||||
src/ccw_litellm/interfaces/llm.py
|
||||
tests/test_interfaces.py
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
2
ccw-litellm/src/ccw_litellm.egg-info/entry_points.txt
Normal file
2
ccw-litellm/src/ccw_litellm.egg-info/entry_points.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
[console_scripts]
|
||||
ccw-litellm = ccw_litellm.cli:main
|
||||
7
ccw-litellm/src/ccw_litellm.egg-info/requires.txt
Normal file
7
ccw-litellm/src/ccw_litellm.egg-info/requires.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
litellm>=1.0.0
|
||||
pyyaml
|
||||
numpy
|
||||
pydantic>=2.0
|
||||
|
||||
[dev]
|
||||
pytest>=7.0
|
||||
1
ccw-litellm/src/ccw_litellm.egg-info/top_level.txt
Normal file
1
ccw-litellm/src/ccw_litellm.egg-info/top_level.txt
Normal file
@@ -0,0 +1 @@
|
||||
ccw_litellm
|
||||
47
ccw-litellm/src/ccw_litellm/__init__.py
Normal file
47
ccw-litellm/src/ccw_litellm/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""ccw-litellm package.
|
||||
|
||||
This package provides a small, stable interface layer around LiteLLM to share
|
||||
between the ccw and codex-lens projects.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .clients import LiteLLMClient, LiteLLMEmbedder
|
||||
from .config import (
|
||||
EmbeddingModelConfig,
|
||||
LiteLLMConfig,
|
||||
LLMModelConfig,
|
||||
ProviderConfig,
|
||||
get_config,
|
||||
load_config,
|
||||
reset_config,
|
||||
)
|
||||
from .interfaces import (
|
||||
AbstractEmbedder,
|
||||
AbstractLLMClient,
|
||||
ChatMessage,
|
||||
LLMResponse,
|
||||
)
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
# Abstract interfaces
|
||||
"AbstractEmbedder",
|
||||
"AbstractLLMClient",
|
||||
"ChatMessage",
|
||||
"LLMResponse",
|
||||
# Client implementations
|
||||
"LiteLLMClient",
|
||||
"LiteLLMEmbedder",
|
||||
# Configuration
|
||||
"LiteLLMConfig",
|
||||
"ProviderConfig",
|
||||
"LLMModelConfig",
|
||||
"EmbeddingModelConfig",
|
||||
"load_config",
|
||||
"get_config",
|
||||
"reset_config",
|
||||
]
|
||||
|
||||
108
ccw-litellm/src/ccw_litellm/cli.py
Normal file
108
ccw-litellm/src/ccw_litellm/cli.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""CLI entry point for ccw-litellm."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Main CLI entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="ccw-litellm",
|
||||
description="Unified LiteLLM interface for ccw and codex-lens",
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
# config command
|
||||
config_parser = subparsers.add_parser("config", help="Show configuration")
|
||||
config_parser.add_argument(
|
||||
"--path",
|
||||
type=Path,
|
||||
help="Configuration file path",
|
||||
)
|
||||
|
||||
# embed command
|
||||
embed_parser = subparsers.add_parser("embed", help="Generate embeddings")
|
||||
embed_parser.add_argument("texts", nargs="+", help="Texts to embed")
|
||||
embed_parser.add_argument(
|
||||
"--model",
|
||||
default="default",
|
||||
help="Embedding model name (default: default)",
|
||||
)
|
||||
embed_parser.add_argument(
|
||||
"--output",
|
||||
choices=["json", "shape"],
|
||||
default="shape",
|
||||
help="Output format (default: shape)",
|
||||
)
|
||||
|
||||
# chat command
|
||||
chat_parser = subparsers.add_parser("chat", help="Chat with LLM")
|
||||
chat_parser.add_argument("message", help="Message to send")
|
||||
chat_parser.add_argument(
|
||||
"--model",
|
||||
default="default",
|
||||
help="LLM model name (default: default)",
|
||||
)
|
||||
|
||||
# version command
|
||||
subparsers.add_parser("version", help="Show version")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "version":
|
||||
from . import __version__
|
||||
|
||||
print(f"ccw-litellm {__version__}")
|
||||
return 0
|
||||
|
||||
if args.command == "config":
|
||||
from .config import get_config
|
||||
|
||||
try:
|
||||
config = get_config(config_path=args.path if hasattr(args, "path") else None)
|
||||
print(config.model_dump_json(indent=2))
|
||||
except Exception as e:
|
||||
print(f"Error loading config: {e}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
if args.command == "embed":
|
||||
from .clients import LiteLLMEmbedder
|
||||
|
||||
try:
|
||||
embedder = LiteLLMEmbedder(model=args.model)
|
||||
vectors = embedder.embed(args.texts)
|
||||
|
||||
if args.output == "json":
|
||||
print(json.dumps(vectors.tolist()))
|
||||
else:
|
||||
print(f"Shape: {vectors.shape}")
|
||||
print(f"Dimensions: {embedder.dimensions}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
if args.command == "chat":
|
||||
from .clients import LiteLLMClient
|
||||
from .interfaces import ChatMessage
|
||||
|
||||
try:
|
||||
client = LiteLLMClient(model=args.model)
|
||||
response = client.chat([ChatMessage(role="user", content=args.message)])
|
||||
print(response.content)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
parser.print_help()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
12
ccw-litellm/src/ccw_litellm/clients/__init__.py
Normal file
12
ccw-litellm/src/ccw_litellm/clients/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Client implementations for ccw-litellm."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .litellm_embedder import LiteLLMEmbedder
|
||||
from .litellm_llm import LiteLLMClient
|
||||
|
||||
__all__ = [
|
||||
"LiteLLMClient",
|
||||
"LiteLLMEmbedder",
|
||||
]
|
||||
|
||||
251
ccw-litellm/src/ccw_litellm/clients/litellm_embedder.py
Normal file
251
ccw-litellm/src/ccw_litellm/clients/litellm_embedder.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""LiteLLM embedder implementation for text embeddings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Sequence
|
||||
|
||||
import litellm
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from ..config import LiteLLMConfig, get_config
|
||||
from ..interfaces.embedder import AbstractEmbedder
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LiteLLMEmbedder(AbstractEmbedder):
|
||||
"""LiteLLM embedder implementation.
|
||||
|
||||
Supports multiple embedding providers (OpenAI, etc.) through LiteLLM's unified interface.
|
||||
|
||||
Example:
|
||||
embedder = LiteLLMEmbedder(model="default")
|
||||
vectors = embedder.embed(["Hello world", "Another text"])
|
||||
print(vectors.shape) # (2, 1536)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "default",
|
||||
config: LiteLLMConfig | None = None,
|
||||
**litellm_kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize LiteLLM embedder.
|
||||
|
||||
Args:
|
||||
model: Model name from configuration (default: "default")
|
||||
config: Configuration instance (default: use global config)
|
||||
**litellm_kwargs: Additional arguments to pass to litellm.embedding()
|
||||
"""
|
||||
self._config = config or get_config()
|
||||
self._model_name = model
|
||||
self._litellm_kwargs = litellm_kwargs
|
||||
|
||||
# Get embedding model configuration
|
||||
try:
|
||||
self._model_config = self._config.get_embedding_model(model)
|
||||
except ValueError as e:
|
||||
logger.error(f"Failed to get embedding model configuration: {e}")
|
||||
raise
|
||||
|
||||
# Get provider configuration
|
||||
try:
|
||||
self._provider_config = self._config.get_provider(self._model_config.provider)
|
||||
except ValueError as e:
|
||||
logger.error(f"Failed to get provider configuration: {e}")
|
||||
raise
|
||||
|
||||
# Set up LiteLLM environment
|
||||
self._setup_litellm()
|
||||
|
||||
def _setup_litellm(self) -> None:
|
||||
"""Configure LiteLLM with provider settings."""
|
||||
provider = self._model_config.provider
|
||||
|
||||
# Set API key
|
||||
if self._provider_config.api_key:
|
||||
litellm.api_key = self._provider_config.api_key
|
||||
# Also set environment-specific keys
|
||||
if provider == "openai":
|
||||
litellm.openai_key = self._provider_config.api_key
|
||||
elif provider == "anthropic":
|
||||
litellm.anthropic_key = self._provider_config.api_key
|
||||
|
||||
# Set API base
|
||||
if self._provider_config.api_base:
|
||||
litellm.api_base = self._provider_config.api_base
|
||||
|
||||
def _format_model_name(self) -> str:
|
||||
"""Format model name for LiteLLM.
|
||||
|
||||
Returns:
|
||||
Formatted model name (e.g., "openai/text-embedding-3-small")
|
||||
"""
|
||||
provider = self._model_config.provider
|
||||
model = self._model_config.model
|
||||
|
||||
# For some providers, LiteLLM expects explicit prefix
|
||||
if provider in ["azure", "vertex_ai", "bedrock"]:
|
||||
return f"{provider}/{model}"
|
||||
|
||||
# For providers with custom api_base (OpenAI-compatible endpoints),
|
||||
# use openai/ prefix to tell LiteLLM to use OpenAI API format
|
||||
if self._provider_config.api_base and provider not in ["openai", "anthropic"]:
|
||||
return f"openai/{model}"
|
||||
|
||||
return model
|
||||
|
||||
@property
|
||||
def dimensions(self) -> int:
|
||||
"""Embedding vector size."""
|
||||
return self._model_config.dimensions
|
||||
|
||||
def _estimate_tokens(self, text: str) -> int:
|
||||
"""Estimate token count for a text using fast heuristic.
|
||||
|
||||
Args:
|
||||
text: Text to estimate tokens for
|
||||
|
||||
Returns:
|
||||
Estimated token count (len/4 is a reasonable approximation)
|
||||
"""
|
||||
return len(text) // 4
|
||||
|
||||
def _create_batches(
|
||||
self,
|
||||
texts: list[str],
|
||||
max_tokens: int = 30000
|
||||
) -> list[list[str]]:
|
||||
"""Split texts into batches that fit within token limits.
|
||||
|
||||
Args:
|
||||
texts: List of texts to batch
|
||||
max_tokens: Maximum tokens per batch (default: 30000, safe margin for 40960 limit)
|
||||
|
||||
Returns:
|
||||
List of text batches
|
||||
"""
|
||||
batches = []
|
||||
current_batch = []
|
||||
current_tokens = 0
|
||||
|
||||
for text in texts:
|
||||
text_tokens = self._estimate_tokens(text)
|
||||
|
||||
# If single text exceeds limit, truncate it
|
||||
if text_tokens > max_tokens:
|
||||
logger.warning(f"Text with {text_tokens} estimated tokens exceeds limit, truncating")
|
||||
# Truncate to fit (rough estimate: 4 chars per token)
|
||||
max_chars = max_tokens * 4
|
||||
text = text[:max_chars]
|
||||
text_tokens = self._estimate_tokens(text)
|
||||
|
||||
# Start new batch if current would exceed limit
|
||||
if current_tokens + text_tokens > max_tokens and current_batch:
|
||||
batches.append(current_batch)
|
||||
current_batch = []
|
||||
current_tokens = 0
|
||||
|
||||
current_batch.append(text)
|
||||
current_tokens += text_tokens
|
||||
|
||||
# Add final batch
|
||||
if current_batch:
|
||||
batches.append(current_batch)
|
||||
|
||||
return batches
|
||||
|
||||
def embed(
|
||||
self,
|
||||
texts: str | Sequence[str],
|
||||
*,
|
||||
batch_size: int | None = None,
|
||||
max_tokens_per_batch: int = 30000,
|
||||
**kwargs: Any,
|
||||
) -> NDArray[np.floating]:
|
||||
"""Embed one or more texts.
|
||||
|
||||
Args:
|
||||
texts: Single text or sequence of texts
|
||||
batch_size: Batch size for processing (deprecated, use max_tokens_per_batch)
|
||||
max_tokens_per_batch: Maximum estimated tokens per API call (default: 30000)
|
||||
**kwargs: Additional arguments for litellm.embedding()
|
||||
|
||||
Returns:
|
||||
A numpy array of shape (n_texts, dimensions).
|
||||
|
||||
Raises:
|
||||
Exception: If LiteLLM embedding fails
|
||||
"""
|
||||
# Normalize input to list
|
||||
if isinstance(texts, str):
|
||||
text_list = [texts]
|
||||
else:
|
||||
text_list = list(texts)
|
||||
|
||||
if not text_list:
|
||||
# Return empty array with correct shape
|
||||
return np.empty((0, self.dimensions), dtype=np.float32)
|
||||
|
||||
# Merge kwargs
|
||||
embedding_kwargs = {**self._litellm_kwargs, **kwargs}
|
||||
|
||||
# For OpenAI-compatible endpoints, ensure encoding_format is set
|
||||
if self._provider_config.api_base and "encoding_format" not in embedding_kwargs:
|
||||
embedding_kwargs["encoding_format"] = "float"
|
||||
|
||||
# Split into token-aware batches
|
||||
batches = self._create_batches(text_list, max_tokens_per_batch)
|
||||
|
||||
if len(batches) > 1:
|
||||
logger.info(f"Split {len(text_list)} texts into {len(batches)} batches for embedding")
|
||||
|
||||
all_embeddings = []
|
||||
|
||||
for batch_idx, batch in enumerate(batches):
|
||||
try:
|
||||
# Build call kwargs with explicit api_base
|
||||
call_kwargs = {**embedding_kwargs}
|
||||
if self._provider_config.api_base:
|
||||
call_kwargs["api_base"] = self._provider_config.api_base
|
||||
if self._provider_config.api_key:
|
||||
call_kwargs["api_key"] = self._provider_config.api_key
|
||||
|
||||
# Call LiteLLM embedding for this batch
|
||||
response = litellm.embedding(
|
||||
model=self._format_model_name(),
|
||||
input=batch,
|
||||
**call_kwargs,
|
||||
)
|
||||
|
||||
# Extract embeddings
|
||||
batch_embeddings = [item["embedding"] for item in response.data]
|
||||
all_embeddings.extend(batch_embeddings)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LiteLLM embedding failed for batch {batch_idx + 1}/{len(batches)}: {e}")
|
||||
raise
|
||||
|
||||
# Convert to numpy array
|
||||
result = np.array(all_embeddings, dtype=np.float32)
|
||||
|
||||
# Validate dimensions
|
||||
if result.shape[1] != self.dimensions:
|
||||
logger.warning(
|
||||
f"Expected {self.dimensions} dimensions, got {result.shape[1]}. "
|
||||
f"Configuration may be incorrect."
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def model_name(self) -> str:
|
||||
"""Get configured model name."""
|
||||
return self._model_name
|
||||
|
||||
@property
|
||||
def provider(self) -> str:
|
||||
"""Get configured provider name."""
|
||||
return self._model_config.provider
|
||||
165
ccw-litellm/src/ccw_litellm/clients/litellm_llm.py
Normal file
165
ccw-litellm/src/ccw_litellm/clients/litellm_llm.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""LiteLLM client implementation for LLM operations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Sequence
|
||||
|
||||
import litellm
|
||||
|
||||
from ..config import LiteLLMConfig, get_config
|
||||
from ..interfaces.llm import AbstractLLMClient, ChatMessage, LLMResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LiteLLMClient(AbstractLLMClient):
|
||||
"""LiteLLM client implementation.
|
||||
|
||||
Supports multiple providers (OpenAI, Anthropic, etc.) through LiteLLM's unified interface.
|
||||
|
||||
Example:
|
||||
client = LiteLLMClient(model="default")
|
||||
response = client.chat([
|
||||
ChatMessage(role="user", content="Hello!")
|
||||
])
|
||||
print(response.content)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "default",
|
||||
config: LiteLLMConfig | None = None,
|
||||
**litellm_kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize LiteLLM client.
|
||||
|
||||
Args:
|
||||
model: Model name from configuration (default: "default")
|
||||
config: Configuration instance (default: use global config)
|
||||
**litellm_kwargs: Additional arguments to pass to litellm.completion()
|
||||
"""
|
||||
self._config = config or get_config()
|
||||
self._model_name = model
|
||||
self._litellm_kwargs = litellm_kwargs
|
||||
|
||||
# Get model configuration
|
||||
try:
|
||||
self._model_config = self._config.get_llm_model(model)
|
||||
except ValueError as e:
|
||||
logger.error(f"Failed to get model configuration: {e}")
|
||||
raise
|
||||
|
||||
# Get provider configuration
|
||||
try:
|
||||
self._provider_config = self._config.get_provider(self._model_config.provider)
|
||||
except ValueError as e:
|
||||
logger.error(f"Failed to get provider configuration: {e}")
|
||||
raise
|
||||
|
||||
# Set up LiteLLM environment
|
||||
self._setup_litellm()
|
||||
|
||||
def _setup_litellm(self) -> None:
|
||||
"""Configure LiteLLM with provider settings."""
|
||||
provider = self._model_config.provider
|
||||
|
||||
# Set API key
|
||||
if self._provider_config.api_key:
|
||||
env_var = f"{provider.upper()}_API_KEY"
|
||||
litellm.api_key = self._provider_config.api_key
|
||||
# Also set environment-specific keys
|
||||
if provider == "openai":
|
||||
litellm.openai_key = self._provider_config.api_key
|
||||
elif provider == "anthropic":
|
||||
litellm.anthropic_key = self._provider_config.api_key
|
||||
|
||||
# Set API base
|
||||
if self._provider_config.api_base:
|
||||
litellm.api_base = self._provider_config.api_base
|
||||
|
||||
def _format_model_name(self) -> str:
|
||||
"""Format model name for LiteLLM.
|
||||
|
||||
Returns:
|
||||
Formatted model name (e.g., "gpt-4", "claude-3-opus-20240229")
|
||||
"""
|
||||
# LiteLLM expects model names in format: "provider/model" or just "model"
|
||||
# If provider is explicit, use provider/model format
|
||||
provider = self._model_config.provider
|
||||
model = self._model_config.model
|
||||
|
||||
# For some providers, LiteLLM expects explicit prefix
|
||||
if provider in ["anthropic", "azure", "vertex_ai", "bedrock"]:
|
||||
return f"{provider}/{model}"
|
||||
|
||||
return model
|
||||
|
||||
def chat(
|
||||
self,
|
||||
messages: Sequence[ChatMessage],
|
||||
**kwargs: Any,
|
||||
) -> LLMResponse:
|
||||
"""Chat completion for a sequence of messages.
|
||||
|
||||
Args:
|
||||
messages: Sequence of chat messages
|
||||
**kwargs: Additional arguments for litellm.completion()
|
||||
|
||||
Returns:
|
||||
LLM response with content and raw response
|
||||
|
||||
Raises:
|
||||
Exception: If LiteLLM completion fails
|
||||
"""
|
||||
# Convert messages to LiteLLM format
|
||||
litellm_messages = [
|
||||
{"role": msg.role, "content": msg.content} for msg in messages
|
||||
]
|
||||
|
||||
# Merge kwargs
|
||||
completion_kwargs = {**self._litellm_kwargs, **kwargs}
|
||||
|
||||
try:
|
||||
# Call LiteLLM
|
||||
response = litellm.completion(
|
||||
model=self._format_model_name(),
|
||||
messages=litellm_messages,
|
||||
**completion_kwargs,
|
||||
)
|
||||
|
||||
# Extract content
|
||||
content = response.choices[0].message.content or ""
|
||||
|
||||
return LLMResponse(content=content, raw=response)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LiteLLM completion failed: {e}")
|
||||
raise
|
||||
|
||||
def complete(self, prompt: str, **kwargs: Any) -> LLMResponse:
|
||||
"""Text completion for a prompt.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt
|
||||
**kwargs: Additional arguments for litellm.completion()
|
||||
|
||||
Returns:
|
||||
LLM response with content and raw response
|
||||
|
||||
Raises:
|
||||
Exception: If LiteLLM completion fails
|
||||
"""
|
||||
# Convert to chat format (most modern models use chat interface)
|
||||
messages = [ChatMessage(role="user", content=prompt)]
|
||||
return self.chat(messages, **kwargs)
|
||||
|
||||
@property
|
||||
def model_name(self) -> str:
|
||||
"""Get configured model name."""
|
||||
return self._model_name
|
||||
|
||||
@property
|
||||
def provider(self) -> str:
|
||||
"""Get configured provider name."""
|
||||
return self._model_config.provider
|
||||
22
ccw-litellm/src/ccw_litellm/config/__init__.py
Normal file
22
ccw-litellm/src/ccw_litellm/config/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Configuration management for LiteLLM integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .loader import get_config, load_config, reset_config
|
||||
from .models import (
|
||||
EmbeddingModelConfig,
|
||||
LiteLLMConfig,
|
||||
LLMModelConfig,
|
||||
ProviderConfig,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LiteLLMConfig",
|
||||
"ProviderConfig",
|
||||
"LLMModelConfig",
|
||||
"EmbeddingModelConfig",
|
||||
"load_config",
|
||||
"get_config",
|
||||
"reset_config",
|
||||
]
|
||||
|
||||
316
ccw-litellm/src/ccw_litellm/config/loader.py
Normal file
316
ccw-litellm/src/ccw_litellm/config/loader.py
Normal file
@@ -0,0 +1,316 @@
|
||||
"""Configuration loader with environment variable substitution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from .models import LiteLLMConfig
|
||||
|
||||
# Default configuration paths
|
||||
# JSON format (UI config) takes priority over YAML format
|
||||
DEFAULT_JSON_CONFIG_PATH = Path.home() / ".ccw" / "config" / "litellm-api-config.json"
|
||||
DEFAULT_YAML_CONFIG_PATH = Path.home() / ".ccw" / "config" / "litellm-config.yaml"
|
||||
# Keep backward compatibility
|
||||
DEFAULT_CONFIG_PATH = DEFAULT_YAML_CONFIG_PATH
|
||||
|
||||
# Global configuration singleton
|
||||
_config_instance: LiteLLMConfig | None = None
|
||||
|
||||
|
||||
def _substitute_env_vars(value: Any) -> Any:
|
||||
"""Recursively substitute environment variables in configuration values.
|
||||
|
||||
Supports ${ENV_VAR} and ${ENV_VAR:-default} syntax.
|
||||
|
||||
Args:
|
||||
value: Configuration value (str, dict, list, or primitive)
|
||||
|
||||
Returns:
|
||||
Value with environment variables substituted
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
# Pattern: ${VAR} or ${VAR:-default}
|
||||
pattern = r"\$\{([^:}]+)(?::-(.*?))?\}"
|
||||
|
||||
def replace_var(match: re.Match) -> str:
|
||||
var_name = match.group(1)
|
||||
default_value = match.group(2) if match.group(2) is not None else ""
|
||||
return os.environ.get(var_name, default_value)
|
||||
|
||||
return re.sub(pattern, replace_var, value)
|
||||
|
||||
if isinstance(value, dict):
|
||||
return {k: _substitute_env_vars(v) for k, v in value.items()}
|
||||
|
||||
if isinstance(value, list):
|
||||
return [_substitute_env_vars(item) for item in value]
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _get_default_config() -> dict[str, Any]:
|
||||
"""Get default configuration when no config file exists.
|
||||
|
||||
Returns:
|
||||
Default configuration dictionary
|
||||
"""
|
||||
return {
|
||||
"version": 1,
|
||||
"default_provider": "openai",
|
||||
"providers": {
|
||||
"openai": {
|
||||
"api_key": "${OPENAI_API_KEY}",
|
||||
"api_base": "https://api.openai.com/v1",
|
||||
},
|
||||
},
|
||||
"llm_models": {
|
||||
"default": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
},
|
||||
"fast": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-3.5-turbo",
|
||||
},
|
||||
},
|
||||
"embedding_models": {
|
||||
"default": {
|
||||
"provider": "openai",
|
||||
"model": "text-embedding-3-small",
|
||||
"dimensions": 1536,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _convert_json_to_internal_format(json_config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Convert UI JSON config format to internal format.
|
||||
|
||||
The UI stores config in a different structure:
|
||||
- providers: array of {id, name, type, apiKey, apiBase, llmModels[], embeddingModels[]}
|
||||
|
||||
Internal format uses:
|
||||
- providers: dict of {provider_id: {api_key, api_base}}
|
||||
- llm_models: dict of {model_id: {provider, model}}
|
||||
- embedding_models: dict of {model_id: {provider, model, dimensions}}
|
||||
|
||||
Args:
|
||||
json_config: Configuration in UI JSON format
|
||||
|
||||
Returns:
|
||||
Configuration in internal format
|
||||
"""
|
||||
providers: dict[str, Any] = {}
|
||||
llm_models: dict[str, Any] = {}
|
||||
embedding_models: dict[str, Any] = {}
|
||||
default_provider: str | None = None
|
||||
|
||||
for provider in json_config.get("providers", []):
|
||||
if not provider.get("enabled", True):
|
||||
continue
|
||||
|
||||
provider_id = provider.get("id", "")
|
||||
if not provider_id:
|
||||
continue
|
||||
|
||||
# Set first enabled provider as default
|
||||
if default_provider is None:
|
||||
default_provider = provider_id
|
||||
|
||||
# Convert provider with advanced settings
|
||||
provider_config: dict[str, Any] = {
|
||||
"api_key": provider.get("apiKey", ""),
|
||||
"api_base": provider.get("apiBase"),
|
||||
}
|
||||
|
||||
# Map advanced settings
|
||||
adv = provider.get("advancedSettings", {})
|
||||
if adv.get("timeout"):
|
||||
provider_config["timeout"] = adv["timeout"]
|
||||
if adv.get("maxRetries"):
|
||||
provider_config["max_retries"] = adv["maxRetries"]
|
||||
if adv.get("organization"):
|
||||
provider_config["organization"] = adv["organization"]
|
||||
if adv.get("apiVersion"):
|
||||
provider_config["api_version"] = adv["apiVersion"]
|
||||
if adv.get("customHeaders"):
|
||||
provider_config["custom_headers"] = adv["customHeaders"]
|
||||
|
||||
providers[provider_id] = provider_config
|
||||
|
||||
# Convert LLM models
|
||||
for model in provider.get("llmModels", []):
|
||||
if not model.get("enabled", True):
|
||||
continue
|
||||
model_id = model.get("id", "")
|
||||
if not model_id:
|
||||
continue
|
||||
|
||||
llm_model_config: dict[str, Any] = {
|
||||
"provider": provider_id,
|
||||
"model": model.get("name", ""),
|
||||
}
|
||||
# Add model-specific endpoint settings
|
||||
endpoint = model.get("endpointSettings", {})
|
||||
if endpoint.get("baseUrl"):
|
||||
llm_model_config["api_base"] = endpoint["baseUrl"]
|
||||
if endpoint.get("timeout"):
|
||||
llm_model_config["timeout"] = endpoint["timeout"]
|
||||
if endpoint.get("maxRetries"):
|
||||
llm_model_config["max_retries"] = endpoint["maxRetries"]
|
||||
|
||||
# Add capabilities
|
||||
caps = model.get("capabilities", {})
|
||||
if caps.get("contextWindow"):
|
||||
llm_model_config["context_window"] = caps["contextWindow"]
|
||||
if caps.get("maxOutputTokens"):
|
||||
llm_model_config["max_output_tokens"] = caps["maxOutputTokens"]
|
||||
|
||||
llm_models[model_id] = llm_model_config
|
||||
|
||||
# Convert embedding models
|
||||
for model in provider.get("embeddingModels", []):
|
||||
if not model.get("enabled", True):
|
||||
continue
|
||||
model_id = model.get("id", "")
|
||||
if not model_id:
|
||||
continue
|
||||
|
||||
embedding_model_config: dict[str, Any] = {
|
||||
"provider": provider_id,
|
||||
"model": model.get("name", ""),
|
||||
"dimensions": model.get("capabilities", {}).get("embeddingDimension", 1536),
|
||||
}
|
||||
# Add model-specific endpoint settings
|
||||
endpoint = model.get("endpointSettings", {})
|
||||
if endpoint.get("baseUrl"):
|
||||
embedding_model_config["api_base"] = endpoint["baseUrl"]
|
||||
if endpoint.get("timeout"):
|
||||
embedding_model_config["timeout"] = endpoint["timeout"]
|
||||
|
||||
embedding_models[model_id] = embedding_model_config
|
||||
|
||||
# Ensure we have defaults if no models found
|
||||
if not llm_models:
|
||||
llm_models["default"] = {
|
||||
"provider": default_provider or "openai",
|
||||
"model": "gpt-4",
|
||||
}
|
||||
|
||||
if not embedding_models:
|
||||
embedding_models["default"] = {
|
||||
"provider": default_provider or "openai",
|
||||
"model": "text-embedding-3-small",
|
||||
"dimensions": 1536,
|
||||
}
|
||||
|
||||
return {
|
||||
"version": json_config.get("version", 1),
|
||||
"default_provider": default_provider or "openai",
|
||||
"providers": providers,
|
||||
"llm_models": llm_models,
|
||||
"embedding_models": embedding_models,
|
||||
}
|
||||
|
||||
|
||||
def load_config(config_path: Path | str | None = None) -> LiteLLMConfig:
|
||||
"""Load LiteLLM configuration from JSON or YAML file.
|
||||
|
||||
Priority order:
|
||||
1. Explicit config_path if provided
|
||||
2. JSON config (UI format): ~/.ccw/config/litellm-api-config.json
|
||||
3. YAML config: ~/.ccw/config/litellm-config.yaml
|
||||
4. Default configuration
|
||||
|
||||
Args:
|
||||
config_path: Path to configuration file (optional)
|
||||
|
||||
Returns:
|
||||
Parsed and validated configuration
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If config file not found and no default available
|
||||
ValueError: If configuration is invalid
|
||||
"""
|
||||
raw_config: dict[str, Any] | None = None
|
||||
is_json_format = False
|
||||
|
||||
if config_path is not None:
|
||||
config_path = Path(config_path)
|
||||
if config_path.exists():
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
if config_path.suffix == ".json":
|
||||
raw_config = json.load(f)
|
||||
is_json_format = True
|
||||
else:
|
||||
raw_config = yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to load configuration from {config_path}: {e}") from e
|
||||
|
||||
# Check JSON config first (UI format)
|
||||
if raw_config is None and DEFAULT_JSON_CONFIG_PATH.exists():
|
||||
try:
|
||||
with open(DEFAULT_JSON_CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||
raw_config = json.load(f)
|
||||
is_json_format = True
|
||||
except Exception:
|
||||
pass # Fall through to YAML
|
||||
|
||||
# Check YAML config
|
||||
if raw_config is None and DEFAULT_YAML_CONFIG_PATH.exists():
|
||||
try:
|
||||
with open(DEFAULT_YAML_CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||
raw_config = yaml.safe_load(f)
|
||||
except Exception:
|
||||
pass # Fall through to default
|
||||
|
||||
# Use default configuration
|
||||
if raw_config is None:
|
||||
raw_config = _get_default_config()
|
||||
|
||||
# Convert JSON format to internal format if needed
|
||||
if is_json_format:
|
||||
raw_config = _convert_json_to_internal_format(raw_config)
|
||||
|
||||
# Substitute environment variables
|
||||
config_data = _substitute_env_vars(raw_config)
|
||||
|
||||
# Validate and parse with Pydantic
|
||||
try:
|
||||
return LiteLLMConfig.model_validate(config_data)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid configuration: {e}") from e
|
||||
|
||||
|
||||
def get_config(config_path: Path | str | None = None, reload: bool = False) -> LiteLLMConfig:
|
||||
"""Get global configuration singleton.
|
||||
|
||||
Args:
|
||||
config_path: Path to configuration file (default: ~/.ccw/config/litellm-config.yaml)
|
||||
reload: Force reload configuration from disk
|
||||
|
||||
Returns:
|
||||
Global configuration instance
|
||||
"""
|
||||
global _config_instance
|
||||
|
||||
if _config_instance is None or reload:
|
||||
_config_instance = load_config(config_path)
|
||||
|
||||
return _config_instance
|
||||
|
||||
|
||||
def reset_config() -> None:
|
||||
"""Reset global configuration singleton.
|
||||
|
||||
Useful for testing.
|
||||
"""
|
||||
global _config_instance
|
||||
_config_instance = None
|
||||
130
ccw-litellm/src/ccw_litellm/config/models.py
Normal file
130
ccw-litellm/src/ccw_litellm/config/models.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Pydantic configuration models for LiteLLM integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ProviderConfig(BaseModel):
|
||||
"""Provider API configuration.
|
||||
|
||||
Supports environment variable substitution in the format ${ENV_VAR}.
|
||||
"""
|
||||
|
||||
api_key: str | None = None
|
||||
api_base: str | None = None
|
||||
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
|
||||
class LLMModelConfig(BaseModel):
|
||||
"""LLM model configuration."""
|
||||
|
||||
provider: str
|
||||
model: str
|
||||
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
|
||||
class EmbeddingModelConfig(BaseModel):
|
||||
"""Embedding model configuration."""
|
||||
|
||||
provider: str # "openai", "fastembed", "ollama", etc.
|
||||
model: str
|
||||
dimensions: int
|
||||
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
|
||||
class LiteLLMConfig(BaseModel):
|
||||
"""Root configuration for LiteLLM integration.
|
||||
|
||||
Example YAML:
|
||||
version: 1
|
||||
default_provider: openai
|
||||
providers:
|
||||
openai:
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
api_base: https://api.openai.com/v1
|
||||
anthropic:
|
||||
api_key: ${ANTHROPIC_API_KEY}
|
||||
llm_models:
|
||||
default:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
fast:
|
||||
provider: openai
|
||||
model: gpt-3.5-turbo
|
||||
embedding_models:
|
||||
default:
|
||||
provider: openai
|
||||
model: text-embedding-3-small
|
||||
dimensions: 1536
|
||||
"""
|
||||
|
||||
version: int = 1
|
||||
default_provider: str = "openai"
|
||||
providers: dict[str, ProviderConfig] = Field(default_factory=dict)
|
||||
llm_models: dict[str, LLMModelConfig] = Field(default_factory=dict)
|
||||
embedding_models: dict[str, EmbeddingModelConfig] = Field(default_factory=dict)
|
||||
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
def get_llm_model(self, model: str = "default") -> LLMModelConfig:
|
||||
"""Get LLM model configuration by name.
|
||||
|
||||
Args:
|
||||
model: Model name or "default"
|
||||
|
||||
Returns:
|
||||
LLM model configuration
|
||||
|
||||
Raises:
|
||||
ValueError: If model not found
|
||||
"""
|
||||
if model not in self.llm_models:
|
||||
raise ValueError(
|
||||
f"LLM model '{model}' not found in configuration. "
|
||||
f"Available models: {list(self.llm_models.keys())}"
|
||||
)
|
||||
return self.llm_models[model]
|
||||
|
||||
def get_embedding_model(self, model: str = "default") -> EmbeddingModelConfig:
|
||||
"""Get embedding model configuration by name.
|
||||
|
||||
Args:
|
||||
model: Model name or "default"
|
||||
|
||||
Returns:
|
||||
Embedding model configuration
|
||||
|
||||
Raises:
|
||||
ValueError: If model not found
|
||||
"""
|
||||
if model not in self.embedding_models:
|
||||
raise ValueError(
|
||||
f"Embedding model '{model}' not found in configuration. "
|
||||
f"Available models: {list(self.embedding_models.keys())}"
|
||||
)
|
||||
return self.embedding_models[model]
|
||||
|
||||
def get_provider(self, provider: str) -> ProviderConfig:
|
||||
"""Get provider configuration by name.
|
||||
|
||||
Args:
|
||||
provider: Provider name
|
||||
|
||||
Returns:
|
||||
Provider configuration
|
||||
|
||||
Raises:
|
||||
ValueError: If provider not found
|
||||
"""
|
||||
if provider not in self.providers:
|
||||
raise ValueError(
|
||||
f"Provider '{provider}' not found in configuration. "
|
||||
f"Available providers: {list(self.providers.keys())}"
|
||||
)
|
||||
return self.providers[provider]
|
||||
14
ccw-litellm/src/ccw_litellm/interfaces/__init__.py
Normal file
14
ccw-litellm/src/ccw_litellm/interfaces/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Abstract interfaces for ccw-litellm."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .embedder import AbstractEmbedder
|
||||
from .llm import AbstractLLMClient, ChatMessage, LLMResponse
|
||||
|
||||
__all__ = [
|
||||
"AbstractEmbedder",
|
||||
"AbstractLLMClient",
|
||||
"ChatMessage",
|
||||
"LLMResponse",
|
||||
]
|
||||
|
||||
52
ccw-litellm/src/ccw_litellm/interfaces/embedder.py
Normal file
52
ccw-litellm/src/ccw_litellm/interfaces/embedder.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Sequence
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
|
||||
class AbstractEmbedder(ABC):
|
||||
"""Embedding interface compatible with fastembed-style embedders.
|
||||
|
||||
Implementers only need to provide the synchronous `embed` method; an
|
||||
asynchronous `aembed` wrapper is provided for convenience.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def dimensions(self) -> int:
|
||||
"""Embedding vector size."""
|
||||
|
||||
@abstractmethod
|
||||
def embed(
|
||||
self,
|
||||
texts: str | Sequence[str],
|
||||
*,
|
||||
batch_size: int | None = None,
|
||||
**kwargs: Any,
|
||||
) -> NDArray[np.floating]:
|
||||
"""Embed one or more texts.
|
||||
|
||||
Returns:
|
||||
A numpy array of shape (n_texts, dimensions).
|
||||
"""
|
||||
|
||||
async def aembed(
|
||||
self,
|
||||
texts: str | Sequence[str],
|
||||
*,
|
||||
batch_size: int | None = None,
|
||||
**kwargs: Any,
|
||||
) -> NDArray[np.floating]:
|
||||
"""Async wrapper around `embed` using a worker thread by default."""
|
||||
|
||||
return await asyncio.to_thread(
|
||||
self.embed,
|
||||
texts,
|
||||
batch_size=batch_size,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
45
ccw-litellm/src/ccw_litellm/interfaces/llm.py
Normal file
45
ccw-litellm/src/ccw_litellm/interfaces/llm.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal, Sequence
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ChatMessage:
|
||||
role: Literal["system", "user", "assistant", "tool"]
|
||||
content: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LLMResponse:
|
||||
content: str
|
||||
raw: Any | None = None
|
||||
|
||||
|
||||
class AbstractLLMClient(ABC):
|
||||
"""LiteLLM-like client interface.
|
||||
|
||||
Implementers only need to provide synchronous methods; async wrappers are
|
||||
provided via `asyncio.to_thread`.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def chat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> LLMResponse:
|
||||
"""Chat completion for a sequence of messages."""
|
||||
|
||||
@abstractmethod
|
||||
def complete(self, prompt: str, **kwargs: Any) -> LLMResponse:
|
||||
"""Text completion for a prompt."""
|
||||
|
||||
async def achat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> LLMResponse:
|
||||
"""Async wrapper around `chat` using a worker thread by default."""
|
||||
|
||||
return await asyncio.to_thread(self.chat, messages, **kwargs)
|
||||
|
||||
async def acomplete(self, prompt: str, **kwargs: Any) -> LLMResponse:
|
||||
"""Async wrapper around `complete` using a worker thread by default."""
|
||||
|
||||
return await asyncio.to_thread(self.complete, prompt, **kwargs)
|
||||
|
||||
11
ccw-litellm/tests/conftest.py
Normal file
11
ccw-litellm/tests/conftest.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def pytest_configure() -> None:
|
||||
project_root = Path(__file__).resolve().parents[1]
|
||||
src_dir = project_root / "src"
|
||||
sys.path.insert(0, str(src_dir))
|
||||
|
||||
64
ccw-litellm/tests/test_interfaces.py
Normal file
64
ccw-litellm/tests/test_interfaces.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any, Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ccw_litellm.interfaces import AbstractEmbedder, AbstractLLMClient, ChatMessage, LLMResponse
|
||||
|
||||
|
||||
class _DummyEmbedder(AbstractEmbedder):
|
||||
@property
|
||||
def dimensions(self) -> int:
|
||||
return 3
|
||||
|
||||
def embed(
|
||||
self,
|
||||
texts: str | Sequence[str],
|
||||
*,
|
||||
batch_size: int | None = None,
|
||||
**kwargs: Any,
|
||||
) -> np.ndarray:
|
||||
if isinstance(texts, str):
|
||||
texts = [texts]
|
||||
_ = batch_size
|
||||
_ = kwargs
|
||||
return np.zeros((len(texts), self.dimensions), dtype=np.float32)
|
||||
|
||||
|
||||
class _DummyLLM(AbstractLLMClient):
|
||||
def chat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> LLMResponse:
|
||||
_ = kwargs
|
||||
return LLMResponse(content="".join(m.content for m in messages))
|
||||
|
||||
def complete(self, prompt: str, **kwargs: Any) -> LLMResponse:
|
||||
_ = kwargs
|
||||
return LLMResponse(content=prompt)
|
||||
|
||||
|
||||
def test_embed_sync_shape_and_dtype() -> None:
|
||||
emb = _DummyEmbedder()
|
||||
out = emb.embed(["a", "b"])
|
||||
assert out.shape == (2, 3)
|
||||
assert out.dtype == np.float32
|
||||
|
||||
|
||||
def test_embed_async_wrapper() -> None:
|
||||
emb = _DummyEmbedder()
|
||||
out = asyncio.run(emb.aembed("x"))
|
||||
assert out.shape == (1, 3)
|
||||
|
||||
|
||||
def test_llm_sync() -> None:
|
||||
llm = _DummyLLM()
|
||||
out = llm.chat([ChatMessage(role="user", content="hi")])
|
||||
assert out == LLMResponse(content="hi")
|
||||
|
||||
|
||||
def test_llm_async_wrappers() -> None:
|
||||
llm = _DummyLLM()
|
||||
out1 = asyncio.run(llm.achat([ChatMessage(role="user", content="a")]))
|
||||
out2 = asyncio.run(llm.acomplete("b"))
|
||||
assert out1.content == "a"
|
||||
assert out2.content == "b"
|
||||
1
ccw/.gitignore
vendored
1
ccw/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
|
||||
# TypeScript build output
|
||||
dist/
|
||||
.ace-tool/
|
||||
|
||||
308
ccw/LITELLM_INTEGRATION.md
Normal file
308
ccw/LITELLM_INTEGRATION.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# LiteLLM Integration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
CCW now supports custom LiteLLM endpoints with integrated context caching. You can configure multiple providers (OpenAI, Anthropic, Ollama, etc.) and create custom endpoints with file-based caching strategies.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CLI Executor │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌──────────────────────────────┐ │
|
||||
│ │ --model │────────>│ Route Decision: │ │
|
||||
│ │ flag │ │ - gemini/qwen/codex → CLI │ │
|
||||
│ └─────────────┘ │ - custom ID → LiteLLM │ │
|
||||
│ └──────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ LiteLLM Executor │
|
||||
│ │
|
||||
│ 1. Load endpoint config (litellm-api-config.json) │
|
||||
│ 2. Extract @patterns from prompt │
|
||||
│ 3. Pack files via context-cache │
|
||||
│ 4. Call LiteLLM client with cached content + prompt │
|
||||
│ 5. Return result │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### File Location
|
||||
|
||||
Configuration is stored per-project:
|
||||
```
|
||||
<project>/.ccw/storage/config/litellm-api-config.json
|
||||
```
|
||||
|
||||
### Configuration Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"providers": [
|
||||
{
|
||||
"id": "openai-1234567890",
|
||||
"name": "My OpenAI",
|
||||
"type": "openai",
|
||||
"apiKey": "${OPENAI_API_KEY}",
|
||||
"enabled": true,
|
||||
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"endpoints": [
|
||||
{
|
||||
"id": "my-gpt4o",
|
||||
"name": "GPT-4o with Context Cache",
|
||||
"providerId": "openai-1234567890",
|
||||
"model": "gpt-4o",
|
||||
"description": "GPT-4o with automatic file caching",
|
||||
"cacheStrategy": {
|
||||
"enabled": true,
|
||||
"ttlMinutes": 60,
|
||||
"maxSizeKB": 512,
|
||||
"filePatterns": ["*.md", "*.ts", "*.js"]
|
||||
},
|
||||
"enabled": true,
|
||||
"createdAt": "2025-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-01-01T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"defaultEndpoint": "my-gpt4o",
|
||||
"globalCacheSettings": {
|
||||
"enabled": true,
|
||||
"cacheDir": "~/.ccw/cache/context",
|
||||
"maxTotalSizeMB": 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Via CLI
|
||||
|
||||
```bash
|
||||
# Use custom endpoint with --model flag
|
||||
ccw cli -p "Analyze authentication flow" --tool litellm --model my-gpt4o
|
||||
|
||||
# With context patterns (automatically cached)
|
||||
ccw cli -p "@src/auth/**/*.ts Review security" --tool litellm --model my-gpt4o
|
||||
|
||||
# Disable caching for specific call
|
||||
ccw cli -p "Quick question" --tool litellm --model my-gpt4o --no-cache
|
||||
```
|
||||
|
||||
### Via Dashboard API
|
||||
|
||||
#### Create Provider
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/litellm-api/providers \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "My OpenAI",
|
||||
"type": "openai",
|
||||
"apiKey": "${OPENAI_API_KEY}",
|
||||
"enabled": true
|
||||
}'
|
||||
```
|
||||
|
||||
#### Create Endpoint
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/litellm-api/endpoints \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id": "my-gpt4o",
|
||||
"name": "GPT-4o with Cache",
|
||||
"providerId": "openai-1234567890",
|
||||
"model": "gpt-4o",
|
||||
"cacheStrategy": {
|
||||
"enabled": true,
|
||||
"ttlMinutes": 60,
|
||||
"maxSizeKB": 512,
|
||||
"filePatterns": ["*.md", "*.ts"]
|
||||
},
|
||||
"enabled": true
|
||||
}'
|
||||
```
|
||||
|
||||
#### Test Provider Connection
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/litellm-api/providers/openai-1234567890/test
|
||||
```
|
||||
|
||||
## Context Caching
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Pattern Detection**: LiteLLM executor scans prompt for `@patterns`
|
||||
```
|
||||
@src/**/*.ts
|
||||
@CLAUDE.md
|
||||
@../shared/**/*
|
||||
```
|
||||
|
||||
2. **File Packing**: Files matching patterns are packed via `context-cache` tool
|
||||
- Respects `max_file_size` limit (default: 1MB per file)
|
||||
- Applies TTL from endpoint config
|
||||
- Generates session ID for retrieval
|
||||
|
||||
3. **Cache Integration**: Cached content is prepended to prompt
|
||||
```
|
||||
<cached files>
|
||||
---
|
||||
<original prompt>
|
||||
```
|
||||
|
||||
4. **LLM Call**: Combined prompt sent to LiteLLM with provider credentials
|
||||
|
||||
### Cache Strategy Configuration
|
||||
|
||||
```typescript
|
||||
interface CacheStrategy {
|
||||
enabled: boolean; // Enable/disable caching for this endpoint
|
||||
ttlMinutes: number; // Cache lifetime (default: 60)
|
||||
maxSizeKB: number; // Max cache size (default: 512KB)
|
||||
filePatterns: string[]; // Glob patterns to cache
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Security Audit with Cache
|
||||
|
||||
```bash
|
||||
ccw cli -p "
|
||||
PURPOSE: OWASP Top 10 security audit of authentication module
|
||||
TASK: • Check SQL injection • Verify session management • Test XSS vectors
|
||||
CONTEXT: @src/auth/**/*.ts @src/middleware/auth.ts
|
||||
EXPECTED: Security report with severity levels and remediation steps
|
||||
" --tool litellm --model my-security-scanner --mode analysis
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Executor detects `@src/auth/**/*.ts` and `@src/middleware/auth.ts`
|
||||
2. Packs matching files into context cache
|
||||
3. Cache entry valid for 60 minutes (per endpoint config)
|
||||
4. Subsequent calls reuse cached files (no re-packing)
|
||||
5. LiteLLM receives full context without manual file specification
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Provider API Keys
|
||||
|
||||
LiteLLM uses standard environment variable names:
|
||||
|
||||
| Provider | Env Var Name |
|
||||
|------------|-----------------------|
|
||||
| OpenAI | `OPENAI_API_KEY` |
|
||||
| Anthropic | `ANTHROPIC_API_KEY` |
|
||||
| Google | `GOOGLE_API_KEY` |
|
||||
| Azure | `AZURE_API_KEY` |
|
||||
| Mistral | `MISTRAL_API_KEY` |
|
||||
| DeepSeek | `DEEPSEEK_API_KEY` |
|
||||
|
||||
### Configuration Syntax
|
||||
|
||||
Use `${ENV_VAR}` syntax in config:
|
||||
```json
|
||||
{
|
||||
"apiKey": "${OPENAI_API_KEY}"
|
||||
}
|
||||
```
|
||||
|
||||
The executor resolves these at runtime via `resolveEnvVar()`.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Config Manager (`litellm-api-config-manager.ts`)
|
||||
|
||||
#### Provider Management
|
||||
```typescript
|
||||
getAllProviders(baseDir: string): ProviderCredential[]
|
||||
getProvider(baseDir: string, providerId: string): ProviderCredential | null
|
||||
getProviderWithResolvedEnvVars(baseDir: string, providerId: string): ProviderCredential & { resolvedApiKey: string } | null
|
||||
addProvider(baseDir: string, providerData): ProviderCredential
|
||||
updateProvider(baseDir: string, providerId: string, updates): ProviderCredential
|
||||
deleteProvider(baseDir: string, providerId: string): boolean
|
||||
```
|
||||
|
||||
#### Endpoint Management
|
||||
```typescript
|
||||
getAllEndpoints(baseDir: string): CustomEndpoint[]
|
||||
getEndpoint(baseDir: string, endpointId: string): CustomEndpoint | null
|
||||
findEndpointById(baseDir: string, endpointId: string): CustomEndpoint | null
|
||||
addEndpoint(baseDir: string, endpointData): CustomEndpoint
|
||||
updateEndpoint(baseDir: string, endpointId: string, updates): CustomEndpoint
|
||||
deleteEndpoint(baseDir: string, endpointId: string): boolean
|
||||
```
|
||||
|
||||
### Executor (`litellm-executor.ts`)
|
||||
|
||||
```typescript
|
||||
interface LiteLLMExecutionOptions {
|
||||
prompt: string;
|
||||
endpointId: string;
|
||||
baseDir: string;
|
||||
cwd?: string;
|
||||
includeDirs?: string[];
|
||||
enableCache?: boolean;
|
||||
onOutput?: (data: { type: string; data: string }) => void;
|
||||
}
|
||||
|
||||
interface LiteLLMExecutionResult {
|
||||
success: boolean;
|
||||
output: string;
|
||||
model: string;
|
||||
provider: string;
|
||||
cacheUsed: boolean;
|
||||
cachedFiles?: string[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
executeLiteLLMEndpoint(options: LiteLLMExecutionOptions): Promise<LiteLLMExecutionResult>
|
||||
extractPatterns(prompt: string): string[]
|
||||
```
|
||||
|
||||
## Dashboard Integration
|
||||
|
||||
The dashboard provides UI for managing LiteLLM configuration:
|
||||
|
||||
- **Providers**: Add/edit/delete provider credentials
|
||||
- **Endpoints**: Configure custom endpoints with cache strategies
|
||||
- **Cache Stats**: View cache usage and clear entries
|
||||
- **Test Connections**: Verify provider API access
|
||||
|
||||
Routes are handled by `litellm-api-routes.ts`.
|
||||
|
||||
## Limitations
|
||||
|
||||
1. **Python Dependency**: Requires `ccw-litellm` Python package installed
|
||||
2. **Model Support**: Limited to models supported by LiteLLM library
|
||||
3. **Cache Scope**: Context cache is in-memory (not persisted across restarts)
|
||||
4. **Pattern Syntax**: Only supports glob-style `@patterns`, not regex
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "Endpoint not found"
|
||||
- Verify endpoint ID matches config file
|
||||
- Check `litellm-api-config.json` exists in `.ccw/storage/config/`
|
||||
|
||||
### Error: "API key not configured"
|
||||
- Ensure environment variable is set
|
||||
- Verify `${ENV_VAR}` syntax in config
|
||||
- Test with `echo $OPENAI_API_KEY`
|
||||
|
||||
### Error: "Failed to spawn Python process"
|
||||
- Install ccw-litellm: `pip install ccw-litellm`
|
||||
- Verify Python accessible: `python --version`
|
||||
|
||||
### Cache Not Applied
|
||||
- Check endpoint has `cacheStrategy.enabled: true`
|
||||
- Verify prompt contains `@patterns`
|
||||
- Check cache TTL hasn't expired
|
||||
|
||||
## Examples
|
||||
|
||||
See `examples/litellm-config.json` for complete configuration template.
|
||||
77
ccw/examples/litellm-usage.ts
Normal file
77
ccw/examples/litellm-usage.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* LiteLLM Usage Examples
|
||||
* Demonstrates how to use the LiteLLM TypeScript client
|
||||
*/
|
||||
|
||||
import { getLiteLLMClient, getLiteLLMStatus } from '../src/tools/litellm-client';
|
||||
|
||||
async function main() {
|
||||
console.log('=== LiteLLM TypeScript Bridge Examples ===\n');
|
||||
|
||||
// Example 1: Check availability
|
||||
console.log('1. Checking LiteLLM availability...');
|
||||
const status = await getLiteLLMStatus();
|
||||
console.log(' Status:', status);
|
||||
console.log('');
|
||||
|
||||
if (!status.available) {
|
||||
console.log('❌ LiteLLM is not available. Please install ccw-litellm:');
|
||||
console.log(' pip install ccw-litellm');
|
||||
return;
|
||||
}
|
||||
|
||||
const client = getLiteLLMClient();
|
||||
|
||||
// Example 2: Get configuration
|
||||
console.log('2. Getting configuration...');
|
||||
try {
|
||||
const config = await client.getConfig();
|
||||
console.log(' Config:', config);
|
||||
} catch (error) {
|
||||
console.log(' Error:', error.message);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Example 3: Generate embeddings
|
||||
console.log('3. Generating embeddings...');
|
||||
try {
|
||||
const texts = ['Hello world', 'Machine learning is amazing'];
|
||||
const embedResult = await client.embed(texts, 'default');
|
||||
console.log(' Dimensions:', embedResult.dimensions);
|
||||
console.log(' Vectors count:', embedResult.vectors.length);
|
||||
console.log(' First vector (first 5 dims):', embedResult.vectors[0]?.slice(0, 5));
|
||||
} catch (error) {
|
||||
console.log(' Error:', error.message);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Example 4: Single message chat
|
||||
console.log('4. Single message chat...');
|
||||
try {
|
||||
const response = await client.chat('What is 2+2?', 'default');
|
||||
console.log(' Response:', response);
|
||||
} catch (error) {
|
||||
console.log(' Error:', error.message);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Example 5: Multi-turn chat
|
||||
console.log('5. Multi-turn chat...');
|
||||
try {
|
||||
const chatResponse = await client.chatMessages([
|
||||
{ role: 'system', content: 'You are a helpful math tutor.' },
|
||||
{ role: 'user', content: 'What is the Pythagorean theorem?' }
|
||||
], 'default');
|
||||
console.log(' Content:', chatResponse.content);
|
||||
console.log(' Model:', chatResponse.model);
|
||||
console.log(' Usage:', chatResponse.usage);
|
||||
} catch (error) {
|
||||
console.log(' Error:', error.message);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
console.log('=== Examples completed ===');
|
||||
}
|
||||
|
||||
// Run examples
|
||||
main().catch(console.error);
|
||||
3854
ccw/package-lock.json
generated
3854
ccw/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -28,20 +28,32 @@ interface PackageInfo {
|
||||
|
||||
/**
|
||||
* Load package.json with error handling
|
||||
* Tries root package.json first (../../package.json from dist),
|
||||
* then falls back to ccw package.json (../package.json from dist)
|
||||
* @returns Package info with version
|
||||
*/
|
||||
function loadPackageInfo(): PackageInfo {
|
||||
const pkgPath = join(__dirname, '../package.json');
|
||||
// First try root package.json (parent of ccw directory)
|
||||
const rootPkgPath = join(__dirname, '../../package.json');
|
||||
// Fallback to ccw package.json
|
||||
const ccwPkgPath = join(__dirname, '../package.json');
|
||||
|
||||
try {
|
||||
if (!existsSync(pkgPath)) {
|
||||
console.error('Fatal Error: package.json not found.');
|
||||
console.error(`Expected location: ${pkgPath}`);
|
||||
process.exit(1);
|
||||
// Try root package.json first
|
||||
if (existsSync(rootPkgPath)) {
|
||||
const content = readFileSync(rootPkgPath, 'utf8');
|
||||
return JSON.parse(content) as PackageInfo;
|
||||
}
|
||||
|
||||
const content = readFileSync(pkgPath, 'utf8');
|
||||
return JSON.parse(content) as PackageInfo;
|
||||
// Fallback to ccw package.json
|
||||
if (existsSync(ccwPkgPath)) {
|
||||
const content = readFileSync(ccwPkgPath, 'utf8');
|
||||
return JSON.parse(content) as PackageInfo;
|
||||
}
|
||||
|
||||
console.error('Fatal Error: package.json not found.');
|
||||
console.error(`Tried locations:\n - ${rootPkgPath}\n - ${ccwPkgPath}`);
|
||||
process.exit(1);
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
console.error('Fatal Error: package.json contains invalid JSON.');
|
||||
@@ -162,20 +174,28 @@ export function run(argv: string[]): void {
|
||||
.option('--cd <path>', 'Working directory')
|
||||
.option('--includeDirs <dirs>', 'Additional directories (--include-directories for gemini/qwen, --add-dir for codex/claude)')
|
||||
.option('--timeout <ms>', 'Timeout in milliseconds', '300000')
|
||||
.option('--no-stream', 'Disable streaming output')
|
||||
.option('--stream', 'Enable streaming output (default: non-streaming with caching)')
|
||||
.option('--limit <n>', 'History limit')
|
||||
.option('--status <status>', 'Filter by status')
|
||||
.option('--category <category>', 'Execution category: user, internal, insight', 'user')
|
||||
.option('--resume [id]', 'Resume previous session (empty=last, or execution ID, or comma-separated IDs for merge)')
|
||||
.option('--id <id>', 'Custom execution ID (e.g., IMPL-001-step1)')
|
||||
.option('--no-native', 'Force prompt concatenation instead of native resume')
|
||||
.option('--cache [items]', 'Cache: comma-separated @patterns and text content')
|
||||
.option('--inject-mode <mode>', 'Inject mode: none, full, progressive (default: codex=full, others=none)')
|
||||
// Storage options
|
||||
.option('--project <path>', 'Project path for storage operations')
|
||||
.option('--force', 'Confirm destructive operations')
|
||||
.option('--cli-history', 'Target CLI history storage')
|
||||
.option('--memory', 'Target memory storage')
|
||||
.option('--cache', 'Target cache storage')
|
||||
.option('--storage-cache', 'Target cache storage')
|
||||
.option('--config', 'Target config storage')
|
||||
// Cache subcommand options
|
||||
.option('--offset <n>', 'Character offset for cache pagination', '0')
|
||||
.option('--output-type <type>', 'Output type: stdout, stderr, both', 'both')
|
||||
.option('--turn <n>', 'Turn number for cache (default: latest)')
|
||||
.option('--raw', 'Raw output only (no formatting)')
|
||||
.option('--final', 'Output final result only with usage hint')
|
||||
.action((subcommand, args, options) => cliCommand(subcommand, args, options));
|
||||
|
||||
// Memory command
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
projectExists,
|
||||
getStorageLocationInstructions
|
||||
} from '../tools/storage-manager.js';
|
||||
import { getHistoryStore } from '../tools/cli-history-store.js';
|
||||
|
||||
// Dashboard notification settings
|
||||
const DASHBOARD_PORT = process.env.CCW_PORT || 3456;
|
||||
@@ -74,10 +75,18 @@ interface CliExecOptions {
|
||||
cd?: string;
|
||||
includeDirs?: string;
|
||||
timeout?: string;
|
||||
noStream?: boolean;
|
||||
stream?: boolean; // Enable streaming (default: false, caches output)
|
||||
resume?: string | boolean; // true = last, string = execution ID, comma-separated for merge
|
||||
id?: string; // Custom execution ID (e.g., IMPL-001-step1)
|
||||
noNative?: boolean; // Force prompt concatenation instead of native resume
|
||||
cache?: string | boolean; // Cache: true = auto from CONTEXT, string = comma-separated patterns/content
|
||||
injectMode?: 'none' | 'full' | 'progressive'; // Inject mode for cached content
|
||||
}
|
||||
|
||||
/** Cache configuration parsed from --cache */
|
||||
interface CacheConfig {
|
||||
patterns?: string[]; // @patterns to pack (items starting with @)
|
||||
content?: string; // Additional text content (items not starting with @)
|
||||
}
|
||||
|
||||
interface HistoryOptions {
|
||||
@@ -91,11 +100,20 @@ interface StorageOptions {
|
||||
project?: string;
|
||||
cliHistory?: boolean;
|
||||
memory?: boolean;
|
||||
cache?: boolean;
|
||||
storageCache?: boolean;
|
||||
config?: boolean;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
interface OutputViewOptions {
|
||||
offset?: string;
|
||||
limit?: string;
|
||||
outputType?: 'stdout' | 'stderr' | 'both';
|
||||
turn?: string;
|
||||
raw?: boolean;
|
||||
final?: boolean; // Only output final result with usage hint
|
||||
}
|
||||
|
||||
/**
|
||||
* Show storage information and management options
|
||||
*/
|
||||
@@ -173,15 +191,15 @@ async function showStorageInfo(): Promise<void> {
|
||||
* Clean storage
|
||||
*/
|
||||
async function cleanStorage(options: StorageOptions): Promise<void> {
|
||||
const { all, project, force, cliHistory, memory, cache, config } = options;
|
||||
const { all, project, force, cliHistory, memory, storageCache, config } = options;
|
||||
|
||||
// Determine what to clean
|
||||
const cleanTypes = {
|
||||
cliHistory: cliHistory || (!cliHistory && !memory && !cache && !config),
|
||||
memory: memory || (!cliHistory && !memory && !cache && !config),
|
||||
cache: cache || (!cliHistory && !memory && !cache && !config),
|
||||
cliHistory: cliHistory || (!cliHistory && !memory && !storageCache && !config),
|
||||
memory: memory || (!cliHistory && !memory && !storageCache && !config),
|
||||
cache: storageCache || (!cliHistory && !memory && !storageCache && !config),
|
||||
config: config || false, // Config requires explicit flag
|
||||
all: !cliHistory && !memory && !cache && !config
|
||||
all: !cliHistory && !memory && !storageCache && !config
|
||||
};
|
||||
|
||||
if (project) {
|
||||
@@ -279,6 +297,86 @@ function showStorageHelp(): void {
|
||||
console.log();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show cached output for a conversation with pagination
|
||||
*/
|
||||
async function outputAction(conversationId: string | undefined, options: OutputViewOptions): Promise<void> {
|
||||
if (!conversationId) {
|
||||
console.error(chalk.red('Error: Conversation ID is required'));
|
||||
console.error(chalk.gray('Usage: ccw cli output <conversation-id> [--offset N] [--limit N]'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const store = getHistoryStore(process.cwd());
|
||||
const result = store.getCachedOutput(
|
||||
conversationId,
|
||||
options.turn ? parseInt(options.turn) : undefined,
|
||||
{
|
||||
offset: parseInt(options.offset || '0'),
|
||||
limit: parseInt(options.limit || '10000'),
|
||||
outputType: options.outputType || 'both'
|
||||
}
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
console.error(chalk.red(`Error: Execution not found: ${conversationId}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.raw) {
|
||||
// Raw output only (for piping)
|
||||
if (result.stdout) console.log(result.stdout.content);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.final) {
|
||||
// Final result only with usage hint
|
||||
if (result.stdout) {
|
||||
console.log(result.stdout.content);
|
||||
}
|
||||
console.log();
|
||||
console.log(chalk.gray('─'.repeat(60)));
|
||||
console.log(chalk.dim(`Usage: ccw cli output ${conversationId} [options]`));
|
||||
console.log(chalk.dim(' --raw Raw output (no hint)'));
|
||||
console.log(chalk.dim(' --offset <n> Start from byte offset'));
|
||||
console.log(chalk.dim(' --limit <n> Limit output bytes'));
|
||||
console.log(chalk.dim(` --resume ccw cli -p "..." --resume ${conversationId}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Formatted output
|
||||
console.log(chalk.bold.cyan('Execution Output\n'));
|
||||
console.log(` ${chalk.gray('ID:')} ${result.conversationId}`);
|
||||
console.log(` ${chalk.gray('Turn:')} ${result.turnNumber}`);
|
||||
console.log(` ${chalk.gray('Cached:')} ${result.cached ? chalk.green('Yes') : chalk.yellow('No')}`);
|
||||
console.log(` ${chalk.gray('Status:')} ${result.status}`);
|
||||
console.log(` ${chalk.gray('Time:')} ${result.timestamp}`);
|
||||
console.log();
|
||||
|
||||
if (result.stdout) {
|
||||
console.log(` ${chalk.gray('Stdout:')} (${result.stdout.totalBytes} bytes, offset ${result.stdout.offset})`);
|
||||
console.log(chalk.gray(' ' + '-'.repeat(60)));
|
||||
console.log(result.stdout.content);
|
||||
console.log(chalk.gray(' ' + '-'.repeat(60)));
|
||||
if (result.stdout.hasMore) {
|
||||
console.log(chalk.yellow(` ... ${result.stdout.totalBytes - result.stdout.offset - result.stdout.content.length} more bytes available`));
|
||||
console.log(chalk.gray(` Use --offset ${result.stdout.offset + result.stdout.content.length} to continue`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (result.stderr && result.stderr.content) {
|
||||
console.log(` ${chalk.gray('Stderr:')} (${result.stderr.totalBytes} bytes, offset ${result.stderr.offset})`);
|
||||
console.log(chalk.gray(' ' + '-'.repeat(60)));
|
||||
console.log(result.stderr.content);
|
||||
console.log(chalk.gray(' ' + '-'.repeat(60)));
|
||||
if (result.stderr.hasMore) {
|
||||
console.log(chalk.yellow(` ... ${result.stderr.totalBytes - result.stderr.offset - result.stderr.content.length} more bytes available`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test endpoint for debugging multi-line prompt parsing
|
||||
* Shows exactly how Commander.js parsed the arguments
|
||||
@@ -383,7 +481,7 @@ async function statusAction(): Promise<void> {
|
||||
* @param {Object} options - CLI options
|
||||
*/
|
||||
async function execAction(positionalPrompt: string | undefined, options: CliExecOptions): Promise<void> {
|
||||
const { prompt: optionPrompt, file, tool = 'gemini', mode = 'analysis', model, cd, includeDirs, timeout, noStream, resume, id, noNative } = options;
|
||||
const { prompt: optionPrompt, file, tool = 'gemini', mode = 'analysis', model, cd, includeDirs, timeout, stream, resume, id, noNative, cache, injectMode } = options;
|
||||
|
||||
// Priority: 1. --file, 2. --prompt/-p option, 3. positional argument
|
||||
let finalPrompt: string | undefined;
|
||||
@@ -421,6 +519,128 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
|
||||
const prompt_to_use = finalPrompt || '';
|
||||
|
||||
// Handle cache option: pack @patterns and/or content
|
||||
let cacheSessionId: string | undefined;
|
||||
let actualPrompt = prompt_to_use;
|
||||
|
||||
if (cache) {
|
||||
const { handler: contextCacheHandler } = await import('../tools/context-cache.js');
|
||||
|
||||
// Parse cache config from comma-separated string
|
||||
// Items starting with @ are patterns, others are text content
|
||||
let cacheConfig: CacheConfig = {};
|
||||
|
||||
if (cache === true) {
|
||||
// --cache without value: auto-extract from CONTEXT field
|
||||
const contextMatch = prompt_to_use.match(/CONTEXT:\s*([^\n]+)/i);
|
||||
if (contextMatch) {
|
||||
const contextLine = contextMatch[1];
|
||||
const patternMatches = contextLine.matchAll(/@[^\s|]+/g);
|
||||
cacheConfig.patterns = Array.from(patternMatches).map(m => m[0]);
|
||||
}
|
||||
} else if (typeof cache === 'string') {
|
||||
// Parse comma-separated items: @patterns and text content
|
||||
const items = cache.split(',').map(s => s.trim()).filter(Boolean);
|
||||
const patterns: string[] = [];
|
||||
const contentParts: string[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (item.startsWith('@')) {
|
||||
patterns.push(item);
|
||||
} else {
|
||||
contentParts.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (patterns.length > 0) {
|
||||
cacheConfig.patterns = patterns;
|
||||
}
|
||||
if (contentParts.length > 0) {
|
||||
cacheConfig.content = contentParts.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Also extract patterns from CONTEXT if not provided
|
||||
if ((!cacheConfig.patterns || cacheConfig.patterns.length === 0) && prompt_to_use) {
|
||||
const contextMatch = prompt_to_use.match(/CONTEXT:\s*([^\n]+)/i);
|
||||
if (contextMatch) {
|
||||
const contextLine = contextMatch[1];
|
||||
const patternMatches = contextLine.matchAll(/@[^\s|]+/g);
|
||||
cacheConfig.patterns = Array.from(patternMatches).map(m => m[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Pack if we have patterns or content
|
||||
if ((cacheConfig.patterns && cacheConfig.patterns.length > 0) || cacheConfig.content) {
|
||||
const patternCount = cacheConfig.patterns?.length || 0;
|
||||
const hasContent = !!cacheConfig.content;
|
||||
console.log(chalk.gray(` Caching: ${patternCount} pattern(s)${hasContent ? ' + text content' : ''}...`));
|
||||
|
||||
const cacheResult = await contextCacheHandler({
|
||||
operation: 'pack',
|
||||
patterns: cacheConfig.patterns,
|
||||
content: cacheConfig.content,
|
||||
cwd: cd || process.cwd(),
|
||||
include_dirs: includeDirs ? includeDirs.split(',') : undefined,
|
||||
});
|
||||
|
||||
if (cacheResult.success && cacheResult.result) {
|
||||
const packResult = cacheResult.result as { session_id: string; files_packed: number; total_bytes: number };
|
||||
cacheSessionId = packResult.session_id;
|
||||
console.log(chalk.gray(` Cached: ${packResult.files_packed} files, ${packResult.total_bytes} bytes`));
|
||||
console.log(chalk.gray(` Session: ${cacheSessionId}`));
|
||||
|
||||
// Determine inject mode:
|
||||
// --inject-mode explicitly set > tool default (codex=full, others=none)
|
||||
const effectiveInjectMode = injectMode ?? (tool === 'codex' ? 'full' : 'none');
|
||||
|
||||
if (effectiveInjectMode !== 'none' && cacheSessionId) {
|
||||
if (effectiveInjectMode === 'full') {
|
||||
// Read full cache content
|
||||
const readResult = await contextCacheHandler({
|
||||
operation: 'read',
|
||||
session_id: cacheSessionId,
|
||||
offset: 0,
|
||||
limit: 1024 * 1024, // 1MB max
|
||||
});
|
||||
|
||||
if (readResult.success && readResult.result) {
|
||||
const { content: cachedContent, total_bytes } = readResult.result as { content: string; total_bytes: number };
|
||||
console.log(chalk.gray(` Injecting ${total_bytes} bytes (full mode)...`));
|
||||
actualPrompt = `=== CACHED CONTEXT (${packResult.files_packed} files) ===\n${cachedContent}\n\n=== USER PROMPT ===\n${prompt_to_use}`;
|
||||
}
|
||||
} else if (effectiveInjectMode === 'progressive') {
|
||||
// Progressive mode: read first page only (64KB default)
|
||||
const pageLimit = 65536;
|
||||
const readResult = await contextCacheHandler({
|
||||
operation: 'read',
|
||||
session_id: cacheSessionId,
|
||||
offset: 0,
|
||||
limit: pageLimit,
|
||||
});
|
||||
|
||||
if (readResult.success && readResult.result) {
|
||||
const { content: cachedContent, total_bytes, has_more, next_offset } = readResult.result as {
|
||||
content: string; total_bytes: number; has_more: boolean; next_offset: number | null
|
||||
};
|
||||
console.log(chalk.gray(` Injecting ${cachedContent.length}/${total_bytes} bytes (progressive mode)...`));
|
||||
|
||||
const moreInfo = has_more
|
||||
? `\n[... ${total_bytes - cachedContent.length} more bytes available via: context_cache(operation="read", session_id="${cacheSessionId}", offset=${next_offset}) ...]`
|
||||
: '';
|
||||
|
||||
actualPrompt = `=== CACHED CONTEXT (${packResult.files_packed} files, progressive) ===\n${cachedContent}${moreInfo}\n\n=== USER PROMPT ===\n${prompt_to_use}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
} else {
|
||||
console.log(chalk.yellow(` Cache warning: ${cacheResult.error}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse resume IDs for merge scenario
|
||||
const resumeIds = resume && typeof resume === 'string' ? resume.split(',').map(s => s.trim()).filter(Boolean) : [];
|
||||
const isMerge = resumeIds.length > 1;
|
||||
@@ -454,15 +674,15 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
custom_id: id || null
|
||||
});
|
||||
|
||||
// Streaming output handler
|
||||
const onOutput = noStream ? null : (chunk: any) => {
|
||||
// Streaming output handler - only active when --stream flag is passed
|
||||
const onOutput = stream ? (chunk: any) => {
|
||||
process.stdout.write(chunk.data);
|
||||
};
|
||||
} : null;
|
||||
|
||||
try {
|
||||
const result = await cliExecutorTool.execute({
|
||||
tool,
|
||||
prompt: prompt_to_use,
|
||||
prompt: actualPrompt,
|
||||
mode,
|
||||
model,
|
||||
cd,
|
||||
@@ -470,11 +690,12 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
timeout: timeout ? parseInt(timeout, 10) : 300000,
|
||||
resume,
|
||||
id, // custom execution ID
|
||||
noNative
|
||||
noNative,
|
||||
stream: !!stream // stream=true → streaming enabled, stream=false/undefined → cache output
|
||||
}, onOutput);
|
||||
|
||||
// If not streaming, print output now
|
||||
if (noStream && result.stdout) {
|
||||
// If not streaming (default), print output now
|
||||
if (!stream && result.stdout) {
|
||||
console.log(result.stdout);
|
||||
}
|
||||
|
||||
@@ -497,6 +718,9 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
console.log(chalk.gray(` Total: ${result.conversation.turn_count} turns, ${(result.conversation.total_duration_ms / 1000).toFixed(1)}s`));
|
||||
}
|
||||
console.log(chalk.dim(` Continue: ccw cli -p "..." --resume ${result.execution.id}`));
|
||||
if (!stream) {
|
||||
console.log(chalk.dim(` Output (optional): ccw cli output ${result.execution.id}`));
|
||||
}
|
||||
|
||||
// Notify dashboard: execution completed
|
||||
notifyDashboard({
|
||||
@@ -556,14 +780,25 @@ async function historyAction(options: HistoryOptions): Promise<void> {
|
||||
|
||||
console.log(chalk.bold.cyan('\n CLI Execution History\n'));
|
||||
|
||||
const history = await getExecutionHistoryAsync(process.cwd(), { limit: parseInt(limit, 10), tool, status });
|
||||
// Use recursive: true to aggregate history from parent and child projects (matches Dashboard behavior)
|
||||
const history = await getExecutionHistoryAsync(process.cwd(), { limit: parseInt(limit, 10), tool, status, recursive: true });
|
||||
|
||||
if (history.executions.length === 0) {
|
||||
console.log(chalk.gray(' No executions found.\n'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.gray(` Total executions: ${history.total}\n`));
|
||||
// Count by tool
|
||||
const toolCounts: Record<string, number> = {};
|
||||
for (const exec of history.executions) {
|
||||
toolCounts[exec.tool] = (toolCounts[exec.tool] || 0) + 1;
|
||||
}
|
||||
const toolSummary = Object.entries(toolCounts).map(([t, c]) => `${t}:${c}`).join(' ');
|
||||
|
||||
// Compact table header with tool breakdown
|
||||
console.log(chalk.gray(` Total: ${history.total} | Showing: ${history.executions.length} (${toolSummary})\n`));
|
||||
console.log(chalk.gray(' Status Tool Time Duration ID'));
|
||||
console.log(chalk.gray(' ' + '─'.repeat(70)));
|
||||
|
||||
for (const exec of history.executions) {
|
||||
const statusIcon = exec.status === 'success' ? chalk.green('●') :
|
||||
@@ -573,13 +808,21 @@ async function historyAction(options: HistoryOptions): Promise<void> {
|
||||
: `${exec.duration_ms}ms`;
|
||||
|
||||
const timeAgo = getTimeAgo(new Date(exec.updated_at || exec.timestamp));
|
||||
const turnInfo = exec.turn_count && exec.turn_count > 1 ? chalk.cyan(` [${exec.turn_count} turns]`) : '';
|
||||
const turnInfo = exec.turn_count && exec.turn_count > 1 ? chalk.cyan(`[${exec.turn_count}t]`) : ' ';
|
||||
|
||||
console.log(` ${statusIcon} ${chalk.bold.white(exec.tool.padEnd(8))} ${chalk.gray(timeAgo.padEnd(12))} ${chalk.gray(duration.padEnd(8))}${turnInfo}`);
|
||||
console.log(chalk.gray(` ${exec.prompt_preview}`));
|
||||
console.log(chalk.dim(` ID: ${exec.id}`));
|
||||
console.log();
|
||||
// Compact format: status tool time duration [turns] + id on same line (no truncation)
|
||||
// Truncate prompt preview to 50 chars for compact display
|
||||
const shortPrompt = exec.prompt_preview.replace(/\n/g, ' ').substring(0, 50).trim();
|
||||
console.log(` ${statusIcon} ${chalk.bold.white(exec.tool.padEnd(8))} ${chalk.gray(timeAgo.padEnd(11))} ${chalk.gray(duration.padEnd(8))} ${turnInfo} ${chalk.dim(exec.id)}`);
|
||||
console.log(chalk.gray(` ${shortPrompt}${exec.prompt_preview.length > 50 ? '...' : ''}`));
|
||||
}
|
||||
|
||||
// Usage hint
|
||||
console.log();
|
||||
console.log(chalk.gray(' ' + '─'.repeat(70)));
|
||||
console.log(chalk.dim(' Filter: ccw cli history --tool <gemini|codex|qwen> --limit <n>'));
|
||||
console.log(chalk.dim(' Output: ccw cli output <id> --final'));
|
||||
console.log();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -685,6 +928,10 @@ export async function cliCommand(
|
||||
await storageAction(argsArray[0], options as unknown as StorageOptions);
|
||||
break;
|
||||
|
||||
case 'output':
|
||||
await outputAction(argsArray[0], options as unknown as OutputViewOptions);
|
||||
break;
|
||||
|
||||
case 'test-parse':
|
||||
// Test endpoint to debug multi-line prompt parsing
|
||||
testParseAction(argsArray, options as CliExecOptions);
|
||||
@@ -715,6 +962,7 @@ export async function cliCommand(
|
||||
console.log(chalk.gray(' storage [cmd] Manage CCW storage (info/clean/config)'));
|
||||
console.log(chalk.gray(' history Show execution history'));
|
||||
console.log(chalk.gray(' detail <id> Show execution detail'));
|
||||
console.log(chalk.gray(' output <id> Show execution output with pagination'));
|
||||
console.log(chalk.gray(' test-parse [args] Debug CLI argument parsing'));
|
||||
console.log();
|
||||
console.log(' Options:');
|
||||
@@ -725,14 +973,35 @@ export async function cliCommand(
|
||||
console.log(chalk.gray(' --model <model> Model override'));
|
||||
console.log(chalk.gray(' --cd <path> Working directory'));
|
||||
console.log(chalk.gray(' --includeDirs <dirs> Additional directories'));
|
||||
console.log(chalk.gray(' --timeout <ms> Timeout (default: 300000)'));
|
||||
console.log(chalk.gray(' --timeout <ms> Timeout (default: 0=disabled)'));
|
||||
console.log(chalk.gray(' --resume [id] Resume previous session'));
|
||||
console.log(chalk.gray(' --cache <items> Cache: comma-separated @patterns and text'));
|
||||
console.log(chalk.gray(' --inject-mode <m> Inject mode: none, full, progressive'));
|
||||
console.log();
|
||||
console.log(' Cache format:');
|
||||
console.log(chalk.gray(' --cache "@src/**/*.ts,@CLAUDE.md" # @patterns to pack'));
|
||||
console.log(chalk.gray(' --cache "@src/**/*,extra context" # patterns + text content'));
|
||||
console.log(chalk.gray(' --cache # auto from CONTEXT field'));
|
||||
console.log();
|
||||
console.log(' Inject modes:');
|
||||
console.log(chalk.gray(' none: cache only, no injection (default for gemini/qwen)'));
|
||||
console.log(chalk.gray(' full: inject all cached content (default for codex)'));
|
||||
console.log(chalk.gray(' progressive: inject first 64KB with MCP continuation hint'));
|
||||
console.log();
|
||||
console.log(' Output options (ccw cli output <id>):');
|
||||
console.log(chalk.gray(' --final Final result only with usage hint'));
|
||||
console.log(chalk.gray(' --raw Raw output only (no formatting, for piping)'));
|
||||
console.log(chalk.gray(' --offset <n> Start from byte offset'));
|
||||
console.log(chalk.gray(' --limit <n> Limit output bytes'));
|
||||
console.log();
|
||||
console.log(' Examples:');
|
||||
console.log(chalk.gray(' ccw cli -p "Analyze auth module" --tool gemini'));
|
||||
console.log(chalk.gray(' ccw cli -f prompt.txt --tool codex --mode write'));
|
||||
console.log(chalk.gray(' ccw cli -p "$(cat template.md)" --tool gemini'));
|
||||
console.log(chalk.gray(' ccw cli --resume --tool gemini'));
|
||||
console.log(chalk.gray(' ccw cli -p "..." --cache "@src/**/*.ts" --tool codex'));
|
||||
console.log(chalk.gray(' ccw cli -p "..." --cache "@src/**/*" --inject-mode progressive --tool gemini'));
|
||||
console.log(chalk.gray(' ccw cli output <id> --final # View result with usage hint'));
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import chalk from 'chalk';
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { homedir } from 'os';
|
||||
|
||||
interface HookOptions {
|
||||
stdin?: boolean;
|
||||
@@ -53,9 +53,10 @@ async function readStdin(): Promise<string> {
|
||||
|
||||
/**
|
||||
* Get session state file path
|
||||
* Uses ~/.claude/.ccw-sessions/ for reliable persistence across sessions
|
||||
*/
|
||||
function getSessionStateFile(sessionId: string): string {
|
||||
const stateDir = join(tmpdir(), '.ccw-sessions');
|
||||
const stateDir = join(homedir(), '.claude', '.ccw-sessions');
|
||||
if (!existsSync(stateDir)) {
|
||||
mkdirSync(stateDir, { recursive: true });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
/**
|
||||
* LiteLLM API Config Manager
|
||||
* Manages provider credentials, endpoint configurations, and model discovery
|
||||
*/
|
||||
|
||||
import { join } from 'path';
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
|
||||
// ===========================
|
||||
// Type Definitions
|
||||
// ===========================
|
||||
|
||||
export type ProviderType =
|
||||
| 'openai'
|
||||
| 'anthropic'
|
||||
| 'google'
|
||||
| 'cohere'
|
||||
| 'azure'
|
||||
| 'bedrock'
|
||||
| 'vertexai'
|
||||
| 'huggingface'
|
||||
| 'ollama'
|
||||
| 'custom';
|
||||
|
||||
export interface ProviderCredential {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ProviderType;
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
apiVersion?: string;
|
||||
region?: string;
|
||||
projectId?: string;
|
||||
organizationId?: string;
|
||||
enabled: boolean;
|
||||
metadata?: Record<string, any>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface EndpointConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
providerId: string;
|
||||
model: string;
|
||||
alias?: string;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
topP?: number;
|
||||
enabled: boolean;
|
||||
metadata?: Record<string, any>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: ProviderType;
|
||||
contextWindow: number;
|
||||
supportsFunctions: boolean;
|
||||
supportsStreaming: boolean;
|
||||
inputCostPer1k?: number;
|
||||
outputCostPer1k?: number;
|
||||
}
|
||||
|
||||
export interface LiteLLMApiConfig {
|
||||
version: string;
|
||||
providers: ProviderCredential[];
|
||||
endpoints: EndpointConfig[];
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Model Definitions
|
||||
// ===========================
|
||||
|
||||
export const PROVIDER_MODELS: Record<ProviderType, ModelInfo[]> = {
|
||||
openai: [
|
||||
{
|
||||
id: 'gpt-4-turbo',
|
||||
name: 'GPT-4 Turbo',
|
||||
provider: 'openai',
|
||||
contextWindow: 128000,
|
||||
supportsFunctions: true,
|
||||
supportsStreaming: true,
|
||||
inputCostPer1k: 0.01,
|
||||
outputCostPer1k: 0.03,
|
||||
},
|
||||
{
|
||||
id: 'gpt-4',
|
||||
name: 'GPT-4',
|
||||
provider: 'openai',
|
||||
contextWindow: 8192,
|
||||
supportsFunctions: true,
|
||||
supportsStreaming: true,
|
||||
inputCostPer1k: 0.03,
|
||||
outputCostPer1k: 0.06,
|
||||
},
|
||||
{
|
||||
id: 'gpt-3.5-turbo',
|
||||
name: 'GPT-3.5 Turbo',
|
||||
provider: 'openai',
|
||||
contextWindow: 16385,
|
||||
supportsFunctions: true,
|
||||
supportsStreaming: true,
|
||||
inputCostPer1k: 0.0005,
|
||||
outputCostPer1k: 0.0015,
|
||||
},
|
||||
],
|
||||
anthropic: [
|
||||
{
|
||||
id: 'claude-3-opus-20240229',
|
||||
name: 'Claude 3 Opus',
|
||||
provider: 'anthropic',
|
||||
contextWindow: 200000,
|
||||
supportsFunctions: true,
|
||||
supportsStreaming: true,
|
||||
inputCostPer1k: 0.015,
|
||||
outputCostPer1k: 0.075,
|
||||
},
|
||||
{
|
||||
id: 'claude-3-sonnet-20240229',
|
||||
name: 'Claude 3 Sonnet',
|
||||
provider: 'anthropic',
|
||||
contextWindow: 200000,
|
||||
supportsFunctions: true,
|
||||
supportsStreaming: true,
|
||||
inputCostPer1k: 0.003,
|
||||
outputCostPer1k: 0.015,
|
||||
},
|
||||
{
|
||||
id: 'claude-3-haiku-20240307',
|
||||
name: 'Claude 3 Haiku',
|
||||
provider: 'anthropic',
|
||||
contextWindow: 200000,
|
||||
supportsFunctions: true,
|
||||
supportsStreaming: true,
|
||||
inputCostPer1k: 0.00025,
|
||||
outputCostPer1k: 0.00125,
|
||||
},
|
||||
],
|
||||
google: [
|
||||
{
|
||||
id: 'gemini-pro',
|
||||
name: 'Gemini Pro',
|
||||
provider: 'google',
|
||||
contextWindow: 32768,
|
||||
supportsFunctions: true,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
{
|
||||
id: 'gemini-pro-vision',
|
||||
name: 'Gemini Pro Vision',
|
||||
provider: 'google',
|
||||
contextWindow: 16384,
|
||||
supportsFunctions: false,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
],
|
||||
cohere: [
|
||||
{
|
||||
id: 'command',
|
||||
name: 'Command',
|
||||
provider: 'cohere',
|
||||
contextWindow: 4096,
|
||||
supportsFunctions: false,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
{
|
||||
id: 'command-light',
|
||||
name: 'Command Light',
|
||||
provider: 'cohere',
|
||||
contextWindow: 4096,
|
||||
supportsFunctions: false,
|
||||
supportsStreaming: true,
|
||||
},
|
||||
],
|
||||
azure: [],
|
||||
bedrock: [],
|
||||
vertexai: [],
|
||||
huggingface: [],
|
||||
ollama: [],
|
||||
custom: [],
|
||||
};
|
||||
|
||||
// ===========================
|
||||
// Config File Management
|
||||
// ===========================
|
||||
|
||||
const CONFIG_DIR = join(homedir(), '.claude', 'litellm');
|
||||
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
||||
|
||||
function ensureConfigDir(): void {
|
||||
if (!existsSync(CONFIG_DIR)) {
|
||||
mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function loadConfig(): LiteLLMApiConfig {
|
||||
ensureConfigDir();
|
||||
|
||||
if (!existsSync(CONFIG_FILE)) {
|
||||
const defaultConfig: LiteLLMApiConfig = {
|
||||
version: '1.0.0',
|
||||
providers: [],
|
||||
endpoints: [],
|
||||
};
|
||||
saveConfig(defaultConfig);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to load config: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function saveConfig(config: LiteLLMApiConfig): void {
|
||||
ensureConfigDir();
|
||||
|
||||
try {
|
||||
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to save config: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Provider Management
|
||||
// ===========================
|
||||
|
||||
export function getAllProviders(): ProviderCredential[] {
|
||||
const config = loadConfig();
|
||||
return config.providers;
|
||||
}
|
||||
|
||||
export function getProvider(id: string): ProviderCredential | null {
|
||||
const config = loadConfig();
|
||||
return config.providers.find((p) => p.id === id) || null;
|
||||
}
|
||||
|
||||
export function createProvider(
|
||||
data: Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>
|
||||
): ProviderCredential {
|
||||
const config = loadConfig();
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const provider: ProviderCredential = {
|
||||
...data,
|
||||
id: `provider-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
config.providers.push(provider);
|
||||
saveConfig(config);
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
export function updateProvider(
|
||||
id: string,
|
||||
updates: Partial<ProviderCredential>
|
||||
): ProviderCredential | null {
|
||||
const config = loadConfig();
|
||||
|
||||
const index = config.providers.findIndex((p) => p.id === id);
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updated: ProviderCredential = {
|
||||
...config.providers[index],
|
||||
...updates,
|
||||
id,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
config.providers[index] = updated;
|
||||
saveConfig(config);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function deleteProvider(id: string): { success: boolean } {
|
||||
const config = loadConfig();
|
||||
|
||||
const index = config.providers.findIndex((p) => p.id === id);
|
||||
if (index === -1) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
config.providers.splice(index, 1);
|
||||
|
||||
// Also delete endpoints using this provider
|
||||
config.endpoints = config.endpoints.filter((e) => e.providerId !== id);
|
||||
|
||||
saveConfig(config);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export async function testProviderConnection(
|
||||
providerId: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const provider = getProvider(providerId);
|
||||
|
||||
if (!provider) {
|
||||
return { success: false, error: 'Provider not found' };
|
||||
}
|
||||
|
||||
if (!provider.enabled) {
|
||||
return { success: false, error: 'Provider is disabled' };
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if (!provider.apiKey && provider.type !== 'ollama' && provider.type !== 'custom') {
|
||||
return { success: false, error: 'API key is required for this provider type' };
|
||||
}
|
||||
|
||||
// TODO: Implement actual provider connection testing using litellm-client
|
||||
// For now, just validate the configuration
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Endpoint Management
|
||||
// ===========================
|
||||
|
||||
export function getAllEndpoints(): EndpointConfig[] {
|
||||
const config = loadConfig();
|
||||
return config.endpoints;
|
||||
}
|
||||
|
||||
export function getEndpoint(id: string): EndpointConfig | null {
|
||||
const config = loadConfig();
|
||||
return config.endpoints.find((e) => e.id === id) || null;
|
||||
}
|
||||
|
||||
export function createEndpoint(
|
||||
data: Omit<EndpointConfig, 'id' | 'createdAt' | 'updatedAt'>
|
||||
): EndpointConfig {
|
||||
const config = loadConfig();
|
||||
|
||||
// Validate provider exists
|
||||
const provider = config.providers.find((p) => p.id === data.providerId);
|
||||
if (!provider) {
|
||||
throw new Error('Provider not found');
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const endpoint: EndpointConfig = {
|
||||
...data,
|
||||
id: `endpoint-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
config.endpoints.push(endpoint);
|
||||
saveConfig(config);
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
export function updateEndpoint(
|
||||
id: string,
|
||||
updates: Partial<EndpointConfig>
|
||||
): EndpointConfig | null {
|
||||
const config = loadConfig();
|
||||
|
||||
const index = config.endpoints.findIndex((e) => e.id === id);
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate provider if being updated
|
||||
if (updates.providerId) {
|
||||
const provider = config.providers.find((p) => p.id === updates.providerId);
|
||||
if (!provider) {
|
||||
throw new Error('Provider not found');
|
||||
}
|
||||
}
|
||||
|
||||
const updated: EndpointConfig = {
|
||||
...config.endpoints[index],
|
||||
...updates,
|
||||
id,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
config.endpoints[index] = updated;
|
||||
saveConfig(config);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function deleteEndpoint(id: string): { success: boolean } {
|
||||
const config = loadConfig();
|
||||
|
||||
const index = config.endpoints.findIndex((e) => e.id === id);
|
||||
if (index === -1) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
config.endpoints.splice(index, 1);
|
||||
saveConfig(config);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Model Discovery
|
||||
// ===========================
|
||||
|
||||
export function getModelsForProviderType(providerType: ProviderType): ModelInfo[] | null {
|
||||
return PROVIDER_MODELS[providerType] || null;
|
||||
}
|
||||
|
||||
export function getAllModels(): Record<ProviderType, ModelInfo[]> {
|
||||
return PROVIDER_MODELS;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Config Access
|
||||
// ===========================
|
||||
|
||||
export function getFullConfig(): LiteLLMApiConfig {
|
||||
return loadConfig();
|
||||
}
|
||||
|
||||
export function resetConfig(): void {
|
||||
const defaultConfig: LiteLLMApiConfig = {
|
||||
version: '1.0.0',
|
||||
providers: [],
|
||||
endpoints: [],
|
||||
};
|
||||
saveConfig(defaultConfig);
|
||||
}
|
||||
1012
ccw/src/config/litellm-api-config-manager.ts
Normal file
1012
ccw/src/config/litellm-api-config-manager.ts
Normal file
File diff suppressed because it is too large
Load Diff
222
ccw/src/config/provider-models.ts
Normal file
222
ccw/src/config/provider-models.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Provider Model Presets
|
||||
*
|
||||
* Predefined model information for each supported LLM provider.
|
||||
* Used for UI dropdowns and validation.
|
||||
*/
|
||||
|
||||
import type { ProviderType } from '../types/litellm-api-config.js';
|
||||
|
||||
/**
|
||||
* Model information metadata
|
||||
*/
|
||||
export interface ModelInfo {
|
||||
/** Model identifier (used in API calls) */
|
||||
id: string;
|
||||
|
||||
/** Human-readable display name */
|
||||
name: string;
|
||||
|
||||
/** Context window size in tokens */
|
||||
contextWindow: number;
|
||||
|
||||
/** Whether this model supports prompt caching */
|
||||
supportsCaching: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Embedding model information metadata
|
||||
*/
|
||||
export interface EmbeddingModelInfo {
|
||||
/** Model identifier (used in API calls) */
|
||||
id: string;
|
||||
|
||||
/** Human-readable display name */
|
||||
name: string;
|
||||
|
||||
/** Embedding dimensions */
|
||||
dimensions: number;
|
||||
|
||||
/** Maximum input tokens */
|
||||
maxTokens: number;
|
||||
|
||||
/** Provider identifier */
|
||||
provider: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Predefined models for each API format
|
||||
* Used for UI selection and validation
|
||||
* Note: Most providers use OpenAI-compatible format
|
||||
*/
|
||||
export const PROVIDER_MODELS: Record<ProviderType, ModelInfo[]> = {
|
||||
// OpenAI-compatible format (used by OpenAI, DeepSeek, Ollama, etc.)
|
||||
openai: [
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
name: 'GPT-4o',
|
||||
contextWindow: 128000,
|
||||
supportsCaching: true
|
||||
},
|
||||
{
|
||||
id: 'gpt-4o-mini',
|
||||
name: 'GPT-4o Mini',
|
||||
contextWindow: 128000,
|
||||
supportsCaching: true
|
||||
},
|
||||
{
|
||||
id: 'o1',
|
||||
name: 'O1',
|
||||
contextWindow: 200000,
|
||||
supportsCaching: true
|
||||
},
|
||||
{
|
||||
id: 'deepseek-chat',
|
||||
name: 'DeepSeek Chat',
|
||||
contextWindow: 64000,
|
||||
supportsCaching: false
|
||||
},
|
||||
{
|
||||
id: 'deepseek-coder',
|
||||
name: 'DeepSeek Coder',
|
||||
contextWindow: 64000,
|
||||
supportsCaching: false
|
||||
},
|
||||
{
|
||||
id: 'llama3.2',
|
||||
name: 'Llama 3.2',
|
||||
contextWindow: 128000,
|
||||
supportsCaching: false
|
||||
},
|
||||
{
|
||||
id: 'qwen2.5-coder',
|
||||
name: 'Qwen 2.5 Coder',
|
||||
contextWindow: 32000,
|
||||
supportsCaching: false
|
||||
}
|
||||
],
|
||||
|
||||
// Anthropic format
|
||||
anthropic: [
|
||||
{
|
||||
id: 'claude-sonnet-4-20250514',
|
||||
name: 'Claude Sonnet 4',
|
||||
contextWindow: 200000,
|
||||
supportsCaching: true
|
||||
},
|
||||
{
|
||||
id: 'claude-3-5-sonnet-20241022',
|
||||
name: 'Claude 3.5 Sonnet',
|
||||
contextWindow: 200000,
|
||||
supportsCaching: true
|
||||
},
|
||||
{
|
||||
id: 'claude-3-5-haiku-20241022',
|
||||
name: 'Claude 3.5 Haiku',
|
||||
contextWindow: 200000,
|
||||
supportsCaching: true
|
||||
},
|
||||
{
|
||||
id: 'claude-3-opus-20240229',
|
||||
name: 'Claude 3 Opus',
|
||||
contextWindow: 200000,
|
||||
supportsCaching: false
|
||||
}
|
||||
],
|
||||
|
||||
// Custom format
|
||||
custom: [
|
||||
{
|
||||
id: 'custom-model',
|
||||
name: 'Custom Model',
|
||||
contextWindow: 128000,
|
||||
supportsCaching: false
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Get models for a specific provider
|
||||
* @param providerType - Provider type to get models for
|
||||
* @returns Array of model information
|
||||
*/
|
||||
export function getModelsForProvider(providerType: ProviderType): ModelInfo[] {
|
||||
return PROVIDER_MODELS[providerType] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Predefined embedding models for each API format
|
||||
* Used for UI selection and validation
|
||||
*/
|
||||
export const EMBEDDING_MODELS: Record<ProviderType, EmbeddingModelInfo[]> = {
|
||||
// OpenAI embedding models
|
||||
openai: [
|
||||
{
|
||||
id: 'text-embedding-3-small',
|
||||
name: 'Text Embedding 3 Small',
|
||||
dimensions: 1536,
|
||||
maxTokens: 8191,
|
||||
provider: 'openai'
|
||||
},
|
||||
{
|
||||
id: 'text-embedding-3-large',
|
||||
name: 'Text Embedding 3 Large',
|
||||
dimensions: 3072,
|
||||
maxTokens: 8191,
|
||||
provider: 'openai'
|
||||
},
|
||||
{
|
||||
id: 'text-embedding-ada-002',
|
||||
name: 'Ada 002',
|
||||
dimensions: 1536,
|
||||
maxTokens: 8191,
|
||||
provider: 'openai'
|
||||
}
|
||||
],
|
||||
|
||||
// Anthropic doesn't have embedding models
|
||||
anthropic: [],
|
||||
|
||||
// Custom embedding models
|
||||
custom: [
|
||||
{
|
||||
id: 'custom-embedding',
|
||||
name: 'Custom Embedding',
|
||||
dimensions: 1536,
|
||||
maxTokens: 8192,
|
||||
provider: 'custom'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Get embedding models for a specific provider
|
||||
* @param providerType - Provider type to get embedding models for
|
||||
* @returns Array of embedding model information
|
||||
*/
|
||||
export function getEmbeddingModelsForProvider(providerType: ProviderType): EmbeddingModelInfo[] {
|
||||
return EMBEDDING_MODELS[providerType] || [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get model information by ID within a provider
|
||||
* @param providerType - Provider type
|
||||
* @param modelId - Model identifier
|
||||
* @returns Model information or undefined if not found
|
||||
*/
|
||||
export function getModelInfo(providerType: ProviderType, modelId: string): ModelInfo | undefined {
|
||||
const models = PROVIDER_MODELS[providerType] || [];
|
||||
return models.find(m => m.id === modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a model ID is supported by a provider
|
||||
* @param providerType - Provider type
|
||||
* @param modelId - Model identifier to validate
|
||||
* @returns true if model is valid for provider
|
||||
*/
|
||||
export function isValidModel(providerType: ProviderType, modelId: string): boolean {
|
||||
return getModelInfo(providerType, modelId) !== undefined;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from 'fs';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync, unlinkSync, readdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { StoragePaths, ensureStorageDir } from '../config/storage-paths.js';
|
||||
|
||||
@@ -118,8 +118,7 @@ export class CacheManager<T> {
|
||||
invalidate(): void {
|
||||
try {
|
||||
if (existsSync(this.cacheFile)) {
|
||||
const fs = require('fs');
|
||||
fs.unlinkSync(this.cacheFile);
|
||||
unlinkSync(this.cacheFile);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Cache invalidation error for ${this.cacheFile}:`, (err as Error).message);
|
||||
@@ -180,8 +179,7 @@ export class CacheManager<T> {
|
||||
if (depth > 3) return; // Limit recursion depth
|
||||
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dirPath, entry.name);
|
||||
|
||||
@@ -46,7 +46,8 @@ const MODULE_CSS_FILES = [
|
||||
'27-graph-explorer.css',
|
||||
'28-mcp-manager.css',
|
||||
'29-help.css',
|
||||
'30-core-memory.css'
|
||||
'30-core-memory.css',
|
||||
'31-api-settings.css'
|
||||
];
|
||||
|
||||
const MODULE_FILES = [
|
||||
@@ -95,6 +96,7 @@ const MODULE_FILES = [
|
||||
'views/skills-manager.js',
|
||||
'views/rules-manager.js',
|
||||
'views/claude-manager.js',
|
||||
'views/api-settings.js',
|
||||
'views/help.js',
|
||||
'main.js'
|
||||
];
|
||||
|
||||
@@ -33,6 +33,17 @@ import {
|
||||
getFullConfigResponse,
|
||||
PREDEFINED_MODELS
|
||||
} from '../../tools/cli-config-manager.js';
|
||||
import {
|
||||
loadClaudeCliTools,
|
||||
saveClaudeCliTools,
|
||||
updateClaudeToolEnabled,
|
||||
updateClaudeCacheSettings,
|
||||
getClaudeCliToolsInfo,
|
||||
addClaudeCustomEndpoint,
|
||||
removeClaudeCustomEndpoint,
|
||||
updateCodeIndexMcp,
|
||||
getCodeIndexMcp
|
||||
} from '../../tools/claude-cli-tools.js';
|
||||
|
||||
export interface RouteContext {
|
||||
pathname: string;
|
||||
@@ -204,6 +215,93 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
// API: Get all custom endpoints
|
||||
if (pathname === '/api/cli/endpoints' && req.method === 'GET') {
|
||||
try {
|
||||
const config = loadClaudeCliTools(initialPath);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ endpoints: config.customEndpoints || [] }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Add/Update custom endpoint
|
||||
if (pathname === '/api/cli/endpoints' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
try {
|
||||
const { id, name, enabled } = body as { id: string; name: string; enabled: boolean };
|
||||
if (!id || !name) {
|
||||
return { error: 'id and name are required', status: 400 };
|
||||
}
|
||||
const config = addClaudeCustomEndpoint(initialPath, { id, name, enabled: enabled !== false });
|
||||
|
||||
broadcastToClients({
|
||||
type: 'CLI_ENDPOINT_UPDATED',
|
||||
payload: { endpoint: { id, name, enabled }, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, endpoints: config.customEndpoints };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Update custom endpoint enabled status
|
||||
if (pathname.match(/^\/api\/cli\/endpoints\/[^/]+$/) && req.method === 'PUT') {
|
||||
const endpointId = pathname.split('/').pop() || '';
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
try {
|
||||
const { enabled, name } = body as { enabled?: boolean; name?: string };
|
||||
const config = loadClaudeCliTools(initialPath);
|
||||
const endpoint = config.customEndpoints.find(e => e.id === endpointId);
|
||||
|
||||
if (!endpoint) {
|
||||
return { error: 'Endpoint not found', status: 404 };
|
||||
}
|
||||
|
||||
if (typeof enabled === 'boolean') endpoint.enabled = enabled;
|
||||
if (name) endpoint.name = name;
|
||||
|
||||
saveClaudeCliTools(initialPath, config);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'CLI_ENDPOINT_UPDATED',
|
||||
payload: { endpoint, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, endpoint };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Delete custom endpoint
|
||||
if (pathname.match(/^\/api\/cli\/endpoints\/[^/]+$/) && req.method === 'DELETE') {
|
||||
const endpointId = pathname.split('/').pop() || '';
|
||||
try {
|
||||
const config = removeClaudeCustomEndpoint(initialPath, endpointId);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'CLI_ENDPOINT_DELETED',
|
||||
payload: { endpointId, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, endpoints: config.customEndpoints }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: CLI Execution History
|
||||
if (pathname === '/api/cli/history') {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
@@ -558,5 +656,141 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Get CLI Tools Config from .claude/cli-tools.json (with fallback to global)
|
||||
if (pathname === '/api/cli/tools-config' && req.method === 'GET') {
|
||||
try {
|
||||
const config = loadClaudeCliTools(initialPath);
|
||||
const info = getClaudeCliToolsInfo(initialPath);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
...config,
|
||||
_configInfo: info
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Update CLI Tools Config
|
||||
if (pathname === '/api/cli/tools-config' && req.method === 'PUT') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
try {
|
||||
const updates = body as Partial<any>;
|
||||
const config = loadClaudeCliTools(initialPath);
|
||||
|
||||
// Merge updates
|
||||
const updatedConfig = {
|
||||
...config,
|
||||
...updates,
|
||||
tools: { ...config.tools, ...(updates.tools || {}) },
|
||||
settings: {
|
||||
...config.settings,
|
||||
...(updates.settings || {}),
|
||||
cache: {
|
||||
...config.settings.cache,
|
||||
...(updates.settings?.cache || {})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
saveClaudeCliTools(initialPath, updatedConfig);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'CLI_TOOLS_CONFIG_UPDATED',
|
||||
payload: { config: updatedConfig, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, config: updatedConfig };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Update specific tool enabled status
|
||||
const toolsConfigMatch = pathname.match(/^\/api\/cli\/tools-config\/([a-zA-Z0-9_-]+)$/);
|
||||
if (toolsConfigMatch && req.method === 'PUT') {
|
||||
const toolName = toolsConfigMatch[1];
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
try {
|
||||
const { enabled } = body as { enabled: boolean };
|
||||
const config = updateClaudeToolEnabled(initialPath, toolName, enabled);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'CLI_TOOL_TOGGLED',
|
||||
payload: { tool: toolName, enabled, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, config };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Update cache settings
|
||||
if (pathname === '/api/cli/tools-config/cache' && req.method === 'PUT') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
try {
|
||||
const cacheSettings = body as { injectionMode?: string; defaultPrefix?: string; defaultSuffix?: string };
|
||||
const config = updateClaudeCacheSettings(initialPath, cacheSettings as any);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'CLI_CACHE_SETTINGS_UPDATED',
|
||||
payload: { cache: config.settings.cache, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, config };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Get Code Index MCP provider
|
||||
if (pathname === '/api/cli/code-index-mcp' && req.method === 'GET') {
|
||||
try {
|
||||
const provider = getCodeIndexMcp(initialPath);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ provider }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Update Code Index MCP provider
|
||||
if (pathname === '/api/cli/code-index-mcp' && req.method === 'PUT') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
try {
|
||||
const { provider } = body as { provider: 'codexlens' | 'ace' | 'none' };
|
||||
if (!provider || !['codexlens', 'ace', 'none'].includes(provider)) {
|
||||
return { error: 'Invalid provider. Must be "codexlens", "ace", or "none"', status: 400 };
|
||||
}
|
||||
|
||||
const result = updateCodeIndexMcp(initialPath, provider);
|
||||
|
||||
if (result.success) {
|
||||
broadcastToClients({
|
||||
type: 'CODE_INDEX_MCP_UPDATED',
|
||||
payload: { provider, timestamp: new Date().toISOString() }
|
||||
});
|
||||
return { success: true, provider };
|
||||
} else {
|
||||
return { error: result.error, status: 500 };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
bootstrapVenv,
|
||||
executeCodexLens,
|
||||
checkSemanticStatus,
|
||||
ensureLiteLLMEmbedderReady,
|
||||
installSemantic,
|
||||
detectGpuSupport,
|
||||
uninstallCodexLens,
|
||||
@@ -80,10 +81,22 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
// API: CodexLens Index List - Get all indexed projects with details
|
||||
if (pathname === '/api/codexlens/indexes') {
|
||||
try {
|
||||
// Get config for index directory path
|
||||
const configResult = await executeCodexLens(['config', '--json']);
|
||||
// Check if CodexLens is installed first (without auto-installing)
|
||||
const venvStatus = await checkVenvStatus();
|
||||
if (!venvStatus.ready) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, indexes: [], totalSize: 0, totalSizeFormatted: '0 B' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Execute all CLI commands in parallel
|
||||
const [configResult, projectsResult, statusResult] = await Promise.all([
|
||||
executeCodexLens(['config', '--json']),
|
||||
executeCodexLens(['projects', 'list', '--json']),
|
||||
executeCodexLens(['status', '--json'])
|
||||
]);
|
||||
|
||||
let indexDir = '';
|
||||
|
||||
if (configResult.success) {
|
||||
try {
|
||||
const config = extractJSON(configResult.output);
|
||||
@@ -96,8 +109,6 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
}
|
||||
}
|
||||
|
||||
// Get project list using 'projects list' command
|
||||
const projectsResult = await executeCodexLens(['projects', 'list', '--json']);
|
||||
let indexes: any[] = [];
|
||||
let totalSize = 0;
|
||||
let vectorIndexCount = 0;
|
||||
@@ -107,7 +118,8 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
try {
|
||||
const projectsData = extractJSON(projectsResult.output);
|
||||
if (projectsData.success && Array.isArray(projectsData.result)) {
|
||||
const { statSync, existsSync } = await import('fs');
|
||||
const { stat, readdir } = await import('fs/promises');
|
||||
const { existsSync } = await import('fs');
|
||||
const { basename, join } = await import('path');
|
||||
|
||||
for (const project of projectsData.result) {
|
||||
@@ -128,15 +140,14 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
// Try to get actual index size from index_root
|
||||
if (project.index_root && existsSync(project.index_root)) {
|
||||
try {
|
||||
const { readdirSync } = await import('fs');
|
||||
const files = readdirSync(project.index_root);
|
||||
const files = await readdir(project.index_root);
|
||||
for (const file of files) {
|
||||
try {
|
||||
const filePath = join(project.index_root, file);
|
||||
const stat = statSync(filePath);
|
||||
projectSize += stat.size;
|
||||
if (!lastModified || stat.mtime > lastModified) {
|
||||
lastModified = stat.mtime;
|
||||
const fileStat = await stat(filePath);
|
||||
projectSize += fileStat.size;
|
||||
if (!lastModified || fileStat.mtime > lastModified) {
|
||||
lastModified = fileStat.mtime;
|
||||
}
|
||||
// Check for vector/embedding files
|
||||
if (file.includes('vector') || file.includes('embedding') ||
|
||||
@@ -186,8 +197,7 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
}
|
||||
}
|
||||
|
||||
// Also get summary stats from status command
|
||||
const statusResult = await executeCodexLens(['status', '--json']);
|
||||
// Parse summary stats from status command (already fetched in parallel)
|
||||
let statusSummary: any = {};
|
||||
|
||||
if (statusResult.success) {
|
||||
@@ -242,6 +252,71 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: CodexLens Dashboard Init - Aggregated endpoint for page initialization
|
||||
if (pathname === '/api/codexlens/dashboard-init') {
|
||||
try {
|
||||
const venvStatus = await checkVenvStatus();
|
||||
|
||||
if (!venvStatus.ready) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
installed: false,
|
||||
status: venvStatus,
|
||||
config: { index_dir: '~/.codexlens/indexes', index_count: 0 },
|
||||
semantic: { available: false }
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parallel fetch all initialization data
|
||||
const [configResult, statusResult, semanticStatus] = await Promise.all([
|
||||
executeCodexLens(['config', '--json']),
|
||||
executeCodexLens(['status', '--json']),
|
||||
checkSemanticStatus()
|
||||
]);
|
||||
|
||||
// Parse config
|
||||
let config = { index_dir: '~/.codexlens/indexes', index_count: 0 };
|
||||
if (configResult.success) {
|
||||
try {
|
||||
const configData = extractJSON(configResult.output);
|
||||
if (configData.success && configData.result) {
|
||||
config.index_dir = configData.result.index_dir || configData.result.index_root || config.index_dir;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[CodexLens] Failed to parse config for dashboard init:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse status
|
||||
let statusData: any = {};
|
||||
if (statusResult.success) {
|
||||
try {
|
||||
const status = extractJSON(statusResult.output);
|
||||
if (status.success && status.result) {
|
||||
config.index_count = status.result.projects_count || 0;
|
||||
statusData = status.result;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[CodexLens] Failed to parse status for dashboard init:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
installed: true,
|
||||
status: venvStatus,
|
||||
config,
|
||||
semantic: semanticStatus,
|
||||
statusData
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: err.message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: CodexLens Bootstrap (Install)
|
||||
if (pathname === '/api/codexlens/bootstrap' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async () => {
|
||||
@@ -290,14 +365,24 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
// API: CodexLens Config - GET (Get current configuration with index count)
|
||||
if (pathname === '/api/codexlens/config' && req.method === 'GET') {
|
||||
try {
|
||||
// Check if CodexLens is installed first (without auto-installing)
|
||||
const venvStatus = await checkVenvStatus();
|
||||
|
||||
let responseData = { index_dir: '~/.codexlens/indexes', index_count: 0 };
|
||||
|
||||
// If not installed, return default config without executing CodexLens
|
||||
if (!venvStatus.ready) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(responseData));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fetch both config and status to merge index_count
|
||||
const [configResult, statusResult] = await Promise.all([
|
||||
executeCodexLens(['config', '--json']),
|
||||
executeCodexLens(['status', '--json'])
|
||||
]);
|
||||
|
||||
let responseData = { index_dir: '~/.codexlens/indexes', index_count: 0 };
|
||||
|
||||
// Parse config (extract JSON from output that may contain log messages)
|
||||
if (configResult.success) {
|
||||
try {
|
||||
@@ -388,9 +473,17 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
// API: CodexLens Init (Initialize workspace index)
|
||||
if (pathname === '/api/codexlens/init' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { path: projectPath, indexType = 'vector', embeddingModel = 'code' } = body;
|
||||
const { path: projectPath, indexType = 'vector', embeddingModel = 'code', embeddingBackend = 'fastembed', maxWorkers = 1 } = body;
|
||||
const targetPath = projectPath || initialPath;
|
||||
|
||||
// Ensure LiteLLM backend dependencies are installed before running the CLI
|
||||
if (indexType !== 'normal' && embeddingBackend === 'litellm') {
|
||||
const installResult = await ensureLiteLLMEmbedderReady();
|
||||
if (!installResult.success) {
|
||||
return { success: false, error: installResult.error || 'Failed to prepare LiteLLM embedder', status: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
// Build CLI arguments based on index type
|
||||
const args = ['init', targetPath, '--json'];
|
||||
if (indexType === 'normal') {
|
||||
@@ -398,6 +491,14 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
} else {
|
||||
// Add embedding model selection for vector index
|
||||
args.push('--embedding-model', embeddingModel);
|
||||
// Add embedding backend if not using default fastembed
|
||||
if (embeddingBackend && embeddingBackend !== 'fastembed') {
|
||||
args.push('--embedding-backend', embeddingBackend);
|
||||
}
|
||||
// Add max workers for concurrent API calls (useful for litellm backend)
|
||||
if (maxWorkers && maxWorkers > 1) {
|
||||
args.push('--max-workers', String(maxWorkers));
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast start event
|
||||
@@ -552,6 +653,8 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
const query = url.searchParams.get('query') || '';
|
||||
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
||||
const mode = url.searchParams.get('mode') || 'exact'; // exact, fuzzy, hybrid, vector
|
||||
const maxContentLength = parseInt(url.searchParams.get('max_content_length') || '200', 10);
|
||||
const extraFilesCount = parseInt(url.searchParams.get('extra_files_count') || '10', 10);
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
if (!query) {
|
||||
@@ -561,15 +664,46 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
}
|
||||
|
||||
try {
|
||||
const args = ['search', query, '--path', projectPath, '--limit', limit.toString(), '--mode', mode, '--json'];
|
||||
// Request more results to support split (full content + extra files)
|
||||
const totalToFetch = limit + extraFilesCount;
|
||||
const args = ['search', query, '--path', projectPath, '--limit', totalToFetch.toString(), '--mode', mode, '--json'];
|
||||
|
||||
const result = await executeCodexLens(args, { cwd: projectPath });
|
||||
|
||||
if (result.success) {
|
||||
try {
|
||||
const parsed = extractJSON(result.output);
|
||||
const allResults = parsed.result?.results || [];
|
||||
|
||||
// Truncate content and split results
|
||||
const truncateContent = (content: string | null | undefined): string => {
|
||||
if (!content) return '';
|
||||
if (content.length <= maxContentLength) return content;
|
||||
return content.slice(0, maxContentLength) + '...';
|
||||
};
|
||||
|
||||
// Split results: first N with full content, rest as file paths only
|
||||
const resultsWithContent = allResults.slice(0, limit).map((r: any) => ({
|
||||
...r,
|
||||
content: truncateContent(r.content || r.excerpt),
|
||||
excerpt: truncateContent(r.excerpt || r.content),
|
||||
}));
|
||||
|
||||
const extraResults = allResults.slice(limit, limit + extraFilesCount);
|
||||
const extraFiles = [...new Set(extraResults.map((r: any) => r.path || r.file))];
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, ...parsed.result }));
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
results: resultsWithContent,
|
||||
extra_files: extraFiles.length > 0 ? extraFiles : undefined,
|
||||
metadata: {
|
||||
total: allResults.length,
|
||||
limit,
|
||||
max_content_length: maxContentLength,
|
||||
extra_files_count: extraFilesCount,
|
||||
},
|
||||
}));
|
||||
} catch {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, results: [], output: result.output }));
|
||||
@@ -682,6 +816,87 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: List available GPU devices for selection
|
||||
if (pathname === '/api/codexlens/gpu/list' && req.method === 'GET') {
|
||||
try {
|
||||
// Check if CodexLens is installed first (without auto-installing)
|
||||
const venvStatus = await checkVenvStatus();
|
||||
if (!venvStatus.ready) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, devices: [], selected_device_id: null }));
|
||||
return true;
|
||||
}
|
||||
const result = await executeCodexLens(['gpu-list', '--json']);
|
||||
if (result.success) {
|
||||
try {
|
||||
const parsed = extractJSON(result.output);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(parsed));
|
||||
} catch {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, devices: [], output: result.output }));
|
||||
}
|
||||
} else {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: result.error }));
|
||||
}
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: err.message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Select GPU device for embedding
|
||||
if (pathname === '/api/codexlens/gpu/select' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { device_id } = body;
|
||||
|
||||
if (device_id === undefined || device_id === null) {
|
||||
return { success: false, error: 'device_id is required', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await executeCodexLens(['gpu-select', String(device_id), '--json']);
|
||||
if (result.success) {
|
||||
try {
|
||||
const parsed = extractJSON(result.output);
|
||||
return parsed;
|
||||
} catch {
|
||||
return { success: true, message: 'GPU selected', output: result.output };
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: result.error, status: 500 };
|
||||
}
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Reset GPU selection to auto-detection
|
||||
if (pathname === '/api/codexlens/gpu/reset' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async () => {
|
||||
try {
|
||||
const result = await executeCodexLens(['gpu-reset', '--json']);
|
||||
if (result.success) {
|
||||
try {
|
||||
const parsed = extractJSON(result.output);
|
||||
return parsed;
|
||||
} catch {
|
||||
return { success: true, message: 'GPU selection reset', output: result.output };
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: result.error, status: 500 };
|
||||
}
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: CodexLens Semantic Search Install (with GPU mode support)
|
||||
if (pathname === '/api/codexlens/semantic/install' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
@@ -721,6 +936,13 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
// API: CodexLens Model List (list available embedding models)
|
||||
if (pathname === '/api/codexlens/models' && req.method === 'GET') {
|
||||
try {
|
||||
// Check if CodexLens is installed first (without auto-installing)
|
||||
const venvStatus = await checkVenvStatus();
|
||||
if (!venvStatus.ready) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: 'CodexLens not installed' }));
|
||||
return true;
|
||||
}
|
||||
const result = await executeCodexLens(['model-list', '--json']);
|
||||
if (result.success) {
|
||||
try {
|
||||
|
||||
@@ -31,8 +31,8 @@ const GLOBAL_SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
|
||||
* @returns {string}
|
||||
*/
|
||||
function getProjectSettingsPath(projectPath) {
|
||||
const normalizedPath = projectPath.replace(/\//g, '\\').replace(/^\\([a-zA-Z])\\/, '$1:\\');
|
||||
return join(normalizedPath, '.claude', 'settings.json');
|
||||
// path.join automatically handles cross-platform path separators
|
||||
return join(projectPath, '.claude', 'settings.json');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,29 +181,13 @@ function deleteHookFromSettings(projectPath, scope, event, hookIndex) {
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Session State Tracking (for progressive disclosure)
|
||||
// Session State Tracking
|
||||
// ========================================
|
||||
|
||||
// Track sessions that have received startup context
|
||||
// Key: sessionId, Value: timestamp of first context load
|
||||
const sessionContextState = new Map<string, {
|
||||
firstLoad: string;
|
||||
loadCount: number;
|
||||
lastPrompt?: string;
|
||||
}>();
|
||||
|
||||
// Cleanup old sessions (older than 24 hours)
|
||||
function cleanupOldSessions() {
|
||||
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
||||
for (const [sessionId, state] of sessionContextState.entries()) {
|
||||
if (new Date(state.firstLoad).getTime() < cutoff) {
|
||||
sessionContextState.delete(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run cleanup every hour
|
||||
setInterval(cleanupOldSessions, 60 * 60 * 1000);
|
||||
// NOTE: Session state is managed by the CLI command (src/commands/hook.ts)
|
||||
// using file-based persistence (~/.claude/.ccw-sessions/).
|
||||
// This ensures consistent state tracking across all invocation methods.
|
||||
// The /api/hook endpoint delegates to SessionClusteringService without
|
||||
// managing its own state, as the authoritative state lives in the CLI layer.
|
||||
|
||||
// ========================================
|
||||
// Route Handler
|
||||
@@ -286,7 +270,8 @@ export async function handleHooksRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
}
|
||||
|
||||
// API: Unified Session Context endpoint (Progressive Disclosure)
|
||||
// Automatically detects first prompt vs subsequent prompts
|
||||
// DEPRECATED: Use CLI command `ccw hook session-context --stdin` instead.
|
||||
// This endpoint now uses file-based state (shared with CLI) for consistency.
|
||||
// - First prompt: returns cluster-based session overview
|
||||
// - Subsequent prompts: returns intent-matched sessions based on prompt
|
||||
if (pathname === '/api/hook/session-context' && req.method === 'POST') {
|
||||
@@ -306,21 +291,30 @@ export async function handleHooksRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { SessionClusteringService } = await import('../session-clustering-service.js');
|
||||
const clusteringService = new SessionClusteringService(projectPath);
|
||||
|
||||
// Check if this is the first prompt for this session
|
||||
const existingState = sessionContextState.get(sessionId);
|
||||
// Use file-based session state (shared with CLI hook.ts)
|
||||
const sessionStateDir = join(homedir(), '.claude', '.ccw-sessions');
|
||||
const sessionStateFile = join(sessionStateDir, `session-${sessionId}.json`);
|
||||
|
||||
let existingState: { firstLoad: string; loadCount: number; lastPrompt?: string } | null = null;
|
||||
if (existsSync(sessionStateFile)) {
|
||||
try {
|
||||
existingState = JSON.parse(readFileSync(sessionStateFile, 'utf-8'));
|
||||
} catch {
|
||||
existingState = null;
|
||||
}
|
||||
}
|
||||
|
||||
const isFirstPrompt = !existingState;
|
||||
|
||||
// Update session state
|
||||
if (isFirstPrompt) {
|
||||
sessionContextState.set(sessionId, {
|
||||
firstLoad: new Date().toISOString(),
|
||||
loadCount: 1,
|
||||
lastPrompt: prompt
|
||||
});
|
||||
} else {
|
||||
existingState.loadCount++;
|
||||
existingState.lastPrompt = prompt;
|
||||
// Update session state (file-based)
|
||||
const newState = isFirstPrompt
|
||||
? { firstLoad: new Date().toISOString(), loadCount: 1, lastPrompt: prompt }
|
||||
: { ...existingState!, loadCount: existingState!.loadCount + 1, lastPrompt: prompt };
|
||||
|
||||
if (!existsSync(sessionStateDir)) {
|
||||
mkdirSync(sessionStateDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(sessionStateFile, JSON.stringify(newState, null, 2));
|
||||
|
||||
// Determine which type of context to return
|
||||
let contextType: 'session-start' | 'context';
|
||||
@@ -351,7 +345,7 @@ export async function handleHooksRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
success: true,
|
||||
type: contextType,
|
||||
isFirstPrompt,
|
||||
loadCount: sessionContextState.get(sessionId)?.loadCount || 1,
|
||||
loadCount: newState.loadCount,
|
||||
content,
|
||||
sessionId
|
||||
};
|
||||
|
||||
930
ccw/src/core/routes/litellm-api-routes.ts
Normal file
930
ccw/src/core/routes/litellm-api-routes.ts
Normal file
@@ -0,0 +1,930 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* LiteLLM API Routes Module
|
||||
* Handles LiteLLM provider management, endpoint configuration, and cache management
|
||||
*/
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join as pathJoin } from 'path';
|
||||
|
||||
// Get current module path for package-relative lookups
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
// Package root: routes -> core -> src -> ccw -> package root
|
||||
const PACKAGE_ROOT = pathJoin(__dirname, '..', '..', '..', '..');
|
||||
|
||||
import {
|
||||
getAllProviders,
|
||||
getProvider,
|
||||
addProvider,
|
||||
updateProvider,
|
||||
deleteProvider,
|
||||
getAllEndpoints,
|
||||
getEndpoint,
|
||||
addEndpoint,
|
||||
updateEndpoint,
|
||||
deleteEndpoint,
|
||||
getDefaultEndpoint,
|
||||
setDefaultEndpoint,
|
||||
getGlobalCacheSettings,
|
||||
updateGlobalCacheSettings,
|
||||
loadLiteLLMApiConfig,
|
||||
saveLiteLLMYamlConfig,
|
||||
generateLiteLLMYamlConfig,
|
||||
getCodexLensEmbeddingRotation,
|
||||
updateCodexLensEmbeddingRotation,
|
||||
getEmbeddingProvidersForRotation,
|
||||
generateRotationEndpoints,
|
||||
syncCodexLensConfig,
|
||||
getEmbeddingPoolConfig,
|
||||
updateEmbeddingPoolConfig,
|
||||
discoverProvidersForModel,
|
||||
type ProviderCredential,
|
||||
type CustomEndpoint,
|
||||
type ProviderType,
|
||||
type CodexLensEmbeddingRotation,
|
||||
type EmbeddingPoolConfig,
|
||||
} from '../../config/litellm-api-config-manager.js';
|
||||
import { getContextCacheStore } from '../../tools/context-cache-store.js';
|
||||
import { getLiteLLMClient } from '../../tools/litellm-client.js';
|
||||
|
||||
// Cache for ccw-litellm status check
|
||||
let ccwLitellmStatusCache: {
|
||||
data: { installed: boolean; version?: string; error?: string } | null;
|
||||
timestamp: number;
|
||||
ttl: number;
|
||||
} = {
|
||||
data: null,
|
||||
timestamp: 0,
|
||||
ttl: 5 * 60 * 1000, // 5 minutes
|
||||
};
|
||||
|
||||
// Clear cache (call after install)
|
||||
export function clearCcwLitellmStatusCache() {
|
||||
ccwLitellmStatusCache.data = null;
|
||||
ccwLitellmStatusCache.timestamp = 0;
|
||||
}
|
||||
|
||||
export interface RouteContext {
|
||||
pathname: string;
|
||||
url: URL;
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
initialPath: string;
|
||||
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
|
||||
broadcastToClients: (data: unknown) => void;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Model Information
|
||||
// ===========================
|
||||
|
||||
interface ModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: ProviderType;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const PROVIDER_MODELS: Record<ProviderType, ModelInfo[]> = {
|
||||
openai: [
|
||||
{ id: 'gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'openai', description: '128K context' },
|
||||
{ id: 'gpt-4', name: 'GPT-4', provider: 'openai', description: '8K context' },
|
||||
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo', provider: 'openai', description: '16K context' },
|
||||
],
|
||||
anthropic: [
|
||||
{ id: 'claude-3-opus-20240229', name: 'Claude 3 Opus', provider: 'anthropic', description: '200K context' },
|
||||
{ id: 'claude-3-sonnet-20240229', name: 'Claude 3 Sonnet', provider: 'anthropic', description: '200K context' },
|
||||
{ id: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku', provider: 'anthropic', description: '200K context' },
|
||||
],
|
||||
google: [
|
||||
{ id: 'gemini-pro', name: 'Gemini Pro', provider: 'google', description: '32K context' },
|
||||
{ id: 'gemini-pro-vision', name: 'Gemini Pro Vision', provider: 'google', description: '16K context' },
|
||||
],
|
||||
ollama: [
|
||||
{ id: 'llama2', name: 'Llama 2', provider: 'ollama', description: 'Local model' },
|
||||
{ id: 'mistral', name: 'Mistral', provider: 'ollama', description: 'Local model' },
|
||||
],
|
||||
azure: [],
|
||||
mistral: [
|
||||
{ id: 'mistral-large-latest', name: 'Mistral Large', provider: 'mistral', description: '32K context' },
|
||||
{ id: 'mistral-medium-latest', name: 'Mistral Medium', provider: 'mistral', description: '32K context' },
|
||||
],
|
||||
deepseek: [
|
||||
{ id: 'deepseek-chat', name: 'DeepSeek Chat', provider: 'deepseek', description: '64K context' },
|
||||
{ id: 'deepseek-coder', name: 'DeepSeek Coder', provider: 'deepseek', description: '64K context' },
|
||||
],
|
||||
custom: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle LiteLLM API routes
|
||||
* @returns true if route was handled, false otherwise
|
||||
*/
|
||||
export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients } = ctx;
|
||||
|
||||
// ===========================
|
||||
// Provider Management Routes
|
||||
// ===========================
|
||||
|
||||
// GET /api/litellm-api/providers - List all providers
|
||||
if (pathname === '/api/litellm-api/providers' && req.method === 'GET') {
|
||||
try {
|
||||
const providers = getAllProviders(initialPath);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ providers, count: providers.length }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/litellm-api/providers - Create provider
|
||||
if (pathname === '/api/litellm-api/providers' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
const providerData = body as Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>;
|
||||
|
||||
if (!providerData.name || !providerData.type || !providerData.apiKey) {
|
||||
return { error: 'Provider name, type, and apiKey are required', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
const provider = addProvider(initialPath, providerData);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'LITELLM_PROVIDER_CREATED',
|
||||
payload: { provider, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, provider };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/litellm-api/providers/:id - Get provider by ID
|
||||
const providerGetMatch = pathname.match(/^\/api\/litellm-api\/providers\/([^/]+)$/);
|
||||
if (providerGetMatch && req.method === 'GET') {
|
||||
const providerId = providerGetMatch[1];
|
||||
|
||||
try {
|
||||
const provider = getProvider(initialPath, providerId);
|
||||
if (!provider) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Provider not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(provider));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// PUT /api/litellm-api/providers/:id - Update provider
|
||||
const providerUpdateMatch = pathname.match(/^\/api\/litellm-api\/providers\/([^/]+)$/);
|
||||
if (providerUpdateMatch && req.method === 'PUT') {
|
||||
const providerId = providerUpdateMatch[1];
|
||||
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
const updates = body as Partial<Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>>;
|
||||
|
||||
try {
|
||||
const provider = updateProvider(initialPath, providerId, updates);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'LITELLM_PROVIDER_UPDATED',
|
||||
payload: { provider, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, provider };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 404 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// DELETE /api/litellm-api/providers/:id - Delete provider
|
||||
const providerDeleteMatch = pathname.match(/^\/api\/litellm-api\/providers\/([^/]+)$/);
|
||||
if (providerDeleteMatch && req.method === 'DELETE') {
|
||||
const providerId = providerDeleteMatch[1];
|
||||
|
||||
try {
|
||||
const success = deleteProvider(initialPath, providerId);
|
||||
|
||||
if (!success) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Provider not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
broadcastToClients({
|
||||
type: 'LITELLM_PROVIDER_DELETED',
|
||||
payload: { providerId, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, message: 'Provider deleted' }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/litellm-api/providers/:id/test - Test provider connection
|
||||
const providerTestMatch = pathname.match(/^\/api\/litellm-api\/providers\/([^/]+)\/test$/);
|
||||
if (providerTestMatch && req.method === 'POST') {
|
||||
const providerId = providerTestMatch[1];
|
||||
|
||||
try {
|
||||
const provider = getProvider(initialPath, providerId);
|
||||
|
||||
if (!provider) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: 'Provider not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!provider.enabled) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: 'Provider is disabled' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Test connection using litellm client
|
||||
const client = getLiteLLMClient();
|
||||
const available = await client.isAvailable();
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: available, provider: provider.type }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Endpoint Management Routes
|
||||
// ===========================
|
||||
|
||||
// GET /api/litellm-api/endpoints - List all endpoints
|
||||
if (pathname === '/api/litellm-api/endpoints' && req.method === 'GET') {
|
||||
try {
|
||||
const endpoints = getAllEndpoints(initialPath);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ endpoints, count: endpoints.length }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/litellm-api/endpoints - Create endpoint
|
||||
if (pathname === '/api/litellm-api/endpoints' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
const endpointData = body as Omit<CustomEndpoint, 'createdAt' | 'updatedAt'>;
|
||||
|
||||
if (!endpointData.id || !endpointData.name || !endpointData.providerId || !endpointData.model) {
|
||||
return { error: 'Endpoint id, name, providerId, and model are required', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
const endpoint = addEndpoint(initialPath, endpointData);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'LITELLM_ENDPOINT_CREATED',
|
||||
payload: { endpoint, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, endpoint };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/litellm-api/endpoints/:id - Get endpoint by ID
|
||||
const endpointGetMatch = pathname.match(/^\/api\/litellm-api\/endpoints\/([^/]+)$/);
|
||||
if (endpointGetMatch && req.method === 'GET') {
|
||||
const endpointId = endpointGetMatch[1];
|
||||
|
||||
try {
|
||||
const endpoint = getEndpoint(initialPath, endpointId);
|
||||
if (!endpoint) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Endpoint not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(endpoint));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// PUT /api/litellm-api/endpoints/:id - Update endpoint
|
||||
const endpointUpdateMatch = pathname.match(/^\/api\/litellm-api\/endpoints\/([^/]+)$/);
|
||||
if (endpointUpdateMatch && req.method === 'PUT') {
|
||||
const endpointId = endpointUpdateMatch[1];
|
||||
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
const updates = body as Partial<Omit<CustomEndpoint, 'id' | 'createdAt' | 'updatedAt'>>;
|
||||
|
||||
try {
|
||||
const endpoint = updateEndpoint(initialPath, endpointId, updates);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'LITELLM_ENDPOINT_UPDATED',
|
||||
payload: { endpoint, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, endpoint };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 404 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// DELETE /api/litellm-api/endpoints/:id - Delete endpoint
|
||||
const endpointDeleteMatch = pathname.match(/^\/api\/litellm-api\/endpoints\/([^/]+)$/);
|
||||
if (endpointDeleteMatch && req.method === 'DELETE') {
|
||||
const endpointId = endpointDeleteMatch[1];
|
||||
|
||||
try {
|
||||
const success = deleteEndpoint(initialPath, endpointId);
|
||||
|
||||
if (!success) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Endpoint not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
broadcastToClients({
|
||||
type: 'LITELLM_ENDPOINT_DELETED',
|
||||
payload: { endpointId, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, message: 'Endpoint deleted' }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Model Discovery Routes
|
||||
// ===========================
|
||||
|
||||
// GET /api/litellm-api/models/:providerType - Get available models for provider type
|
||||
const modelsMatch = pathname.match(/^\/api\/litellm-api\/models\/([^/]+)$/);
|
||||
if (modelsMatch && req.method === 'GET') {
|
||||
const providerType = modelsMatch[1] as ProviderType;
|
||||
|
||||
try {
|
||||
const models = PROVIDER_MODELS[providerType];
|
||||
|
||||
if (!models) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Provider type not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ providerType, models, count: models.length }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Cache Management Routes
|
||||
// ===========================
|
||||
|
||||
// GET /api/litellm-api/cache/stats - Get cache statistics
|
||||
if (pathname === '/api/litellm-api/cache/stats' && req.method === 'GET') {
|
||||
try {
|
||||
const cacheStore = getContextCacheStore();
|
||||
const stats = cacheStore.getStatus();
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(stats));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/litellm-api/cache/clear - Clear cache
|
||||
if (pathname === '/api/litellm-api/cache/clear' && req.method === 'POST') {
|
||||
try {
|
||||
const cacheStore = getContextCacheStore();
|
||||
const result = cacheStore.clear();
|
||||
|
||||
broadcastToClients({
|
||||
type: 'LITELLM_CACHE_CLEARED',
|
||||
payload: { removed: result.removed, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, removed: result.removed }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Config Management Routes
|
||||
// ===========================
|
||||
|
||||
// GET /api/litellm-api/config - Get full config
|
||||
if (pathname === '/api/litellm-api/config' && req.method === 'GET') {
|
||||
try {
|
||||
const config = loadLiteLLMApiConfig(initialPath);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(config));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// PUT /api/litellm-api/config/cache - Update global cache settings
|
||||
if (pathname === '/api/litellm-api/config/cache' && req.method === 'PUT') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
const settings = body as Partial<{ enabled: boolean; cacheDir: string; maxTotalSizeMB: number }>;
|
||||
|
||||
try {
|
||||
updateGlobalCacheSettings(initialPath, settings);
|
||||
|
||||
const updatedSettings = getGlobalCacheSettings(initialPath);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'LITELLM_CACHE_SETTINGS_UPDATED',
|
||||
payload: { settings: updatedSettings, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, settings: updatedSettings };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// PUT /api/litellm-api/config/default-endpoint - Set default endpoint
|
||||
if (pathname === '/api/litellm-api/config/default-endpoint' && req.method === 'PUT') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
const { endpointId } = body as { endpointId?: string };
|
||||
|
||||
try {
|
||||
setDefaultEndpoint(initialPath, endpointId);
|
||||
|
||||
const defaultEndpoint = getDefaultEndpoint(initialPath);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'LITELLM_DEFAULT_ENDPOINT_UPDATED',
|
||||
payload: { endpointId, defaultEndpoint, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, defaultEndpoint };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Config Sync Routes
|
||||
// ===========================
|
||||
|
||||
// POST /api/litellm-api/config/sync - Sync UI config to ccw_litellm YAML config
|
||||
if (pathname === '/api/litellm-api/config/sync' && req.method === 'POST') {
|
||||
try {
|
||||
const yamlPath = saveLiteLLMYamlConfig(initialPath);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Config synced to ccw_litellm',
|
||||
yamlPath,
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/litellm-api/config/yaml-preview - Preview YAML config without saving
|
||||
if (pathname === '/api/litellm-api/config/yaml-preview' && req.method === 'GET') {
|
||||
try {
|
||||
const yamlConfig = generateLiteLLMYamlConfig(initialPath);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
config: yamlConfig,
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// CCW-LiteLLM Package Management
|
||||
// ===========================
|
||||
|
||||
// GET /api/litellm-api/ccw-litellm/status - Check ccw-litellm installation status
|
||||
// Supports ?refresh=true to bypass cache
|
||||
if (pathname === '/api/litellm-api/ccw-litellm/status' && req.method === 'GET') {
|
||||
const forceRefresh = url.searchParams.get('refresh') === 'true';
|
||||
|
||||
// Check cache first (unless force refresh)
|
||||
if (!forceRefresh && ccwLitellmStatusCache.data &&
|
||||
Date.now() - ccwLitellmStatusCache.timestamp < ccwLitellmStatusCache.ttl) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(ccwLitellmStatusCache.data));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Async check - use pip show for more reliable detection
|
||||
try {
|
||||
const { exec } = await import('child_process');
|
||||
const { promisify } = await import('util');
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
let result: { installed: boolean; version?: string; error?: string } = { installed: false };
|
||||
|
||||
// Method 1: Try pip show ccw-litellm (most reliable)
|
||||
try {
|
||||
const { stdout } = await execAsync('pip show ccw-litellm', {
|
||||
timeout: 10000,
|
||||
windowsHide: true,
|
||||
shell: true,
|
||||
});
|
||||
// Parse version from pip show output
|
||||
const versionMatch = stdout.match(/Version:\s*(.+)/i);
|
||||
if (versionMatch) {
|
||||
result = { installed: true, version: versionMatch[1].trim() };
|
||||
console.log(`[ccw-litellm status] Found via pip show: ${result.version}`);
|
||||
}
|
||||
} catch (pipErr) {
|
||||
console.log('[ccw-litellm status] pip show failed, trying python import...');
|
||||
|
||||
// Method 2: Fallback to Python import
|
||||
const pythonExecutables = ['python', 'python3', 'py'];
|
||||
for (const pythonExe of pythonExecutables) {
|
||||
try {
|
||||
// Use simpler Python code without complex quotes
|
||||
const { stdout } = await execAsync(`${pythonExe} -c "import ccw_litellm; print(ccw_litellm.__version__)"`, {
|
||||
timeout: 5000,
|
||||
windowsHide: true,
|
||||
shell: true,
|
||||
});
|
||||
const version = stdout.trim();
|
||||
if (version) {
|
||||
result = { installed: true, version };
|
||||
console.log(`[ccw-litellm status] Found with ${pythonExe}: ${version}`);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
result.error = (err as Error).message;
|
||||
console.log(`[ccw-litellm status] ${pythonExe} failed:`, result.error.substring(0, 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update cache
|
||||
ccwLitellmStatusCache = {
|
||||
data: result,
|
||||
timestamp: Date.now(),
|
||||
ttl: 5 * 60 * 1000,
|
||||
};
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(result));
|
||||
} catch (err) {
|
||||
const errorResult = { installed: false, error: (err as Error).message };
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(errorResult));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// CodexLens Embedding Rotation Routes
|
||||
// ===========================
|
||||
|
||||
// GET /api/litellm-api/codexlens/rotation - Get rotation config
|
||||
if (pathname === '/api/litellm-api/codexlens/rotation' && req.method === 'GET') {
|
||||
try {
|
||||
const rotationConfig = getCodexLensEmbeddingRotation(initialPath);
|
||||
const availableProviders = getEmbeddingProvidersForRotation(initialPath);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
rotationConfig: rotationConfig || null,
|
||||
availableProviders,
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// PUT /api/litellm-api/codexlens/rotation - Update rotation config
|
||||
if (pathname === '/api/litellm-api/codexlens/rotation' && req.method === 'PUT') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
const rotationConfig = body as CodexLensEmbeddingRotation | null;
|
||||
|
||||
try {
|
||||
const { syncResult } = updateCodexLensEmbeddingRotation(initialPath, rotationConfig || undefined);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'CODEXLENS_ROTATION_UPDATED',
|
||||
payload: { rotationConfig, syncResult, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, rotationConfig, syncResult };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/litellm-api/codexlens/rotation/endpoints - Get generated rotation endpoints
|
||||
if (pathname === '/api/litellm-api/codexlens/rotation/endpoints' && req.method === 'GET') {
|
||||
try {
|
||||
const endpoints = generateRotationEndpoints(initialPath);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
endpoints,
|
||||
count: endpoints.length,
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/litellm-api/codexlens/rotation/sync - Manually sync rotation config to CodexLens
|
||||
if (pathname === '/api/litellm-api/codexlens/rotation/sync' && req.method === 'POST') {
|
||||
try {
|
||||
const syncResult = syncCodexLensConfig(initialPath);
|
||||
|
||||
if (syncResult.success) {
|
||||
broadcastToClients({
|
||||
type: 'CODEXLENS_CONFIG_SYNCED',
|
||||
payload: { ...syncResult, timestamp: new Date().toISOString() }
|
||||
});
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(syncResult));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Embedding Pool Routes (New Generic API)
|
||||
// ===========================
|
||||
|
||||
// GET /api/litellm-api/embedding-pool - Get pool config and available models
|
||||
if (pathname === '/api/litellm-api/embedding-pool' && req.method === 'GET') {
|
||||
try {
|
||||
const poolConfig = getEmbeddingPoolConfig(initialPath);
|
||||
|
||||
// Get list of all available embedding models from all providers
|
||||
const config = loadLiteLLMApiConfig(initialPath);
|
||||
const availableModels: Array<{ modelId: string; modelName: string; providers: string[] }> = [];
|
||||
const modelMap = new Map<string, { modelId: string; modelName: string; providers: string[] }>();
|
||||
|
||||
for (const provider of config.providers) {
|
||||
if (!provider.enabled || !provider.embeddingModels) continue;
|
||||
|
||||
for (const model of provider.embeddingModels) {
|
||||
if (!model.enabled) continue;
|
||||
|
||||
const key = model.id;
|
||||
if (modelMap.has(key)) {
|
||||
modelMap.get(key)!.providers.push(provider.name);
|
||||
} else {
|
||||
modelMap.set(key, {
|
||||
modelId: model.id,
|
||||
modelName: model.name,
|
||||
providers: [provider.name],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
availableModels.push(...Array.from(modelMap.values()));
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
poolConfig: poolConfig || null,
|
||||
availableModels,
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// PUT /api/litellm-api/embedding-pool - Update pool config
|
||||
if (pathname === '/api/litellm-api/embedding-pool' && req.method === 'PUT') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
const poolConfig = body as EmbeddingPoolConfig | null;
|
||||
|
||||
try {
|
||||
const { syncResult } = updateEmbeddingPoolConfig(initialPath, poolConfig || undefined);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'EMBEDDING_POOL_UPDATED',
|
||||
payload: { poolConfig, syncResult, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, poolConfig, syncResult };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/litellm-api/embedding-pool/discover/:model - Preview auto-discovery results
|
||||
const discoverMatch = pathname.match(/^\/api\/litellm-api\/embedding-pool\/discover\/([^/]+)$/);
|
||||
if (discoverMatch && req.method === 'GET') {
|
||||
const targetModel = decodeURIComponent(discoverMatch[1]);
|
||||
|
||||
try {
|
||||
const discovered = discoverProvidersForModel(initialPath, targetModel);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
targetModel,
|
||||
discovered,
|
||||
count: discovered.length,
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/litellm-api/ccw-litellm/install - Install ccw-litellm package
|
||||
if (pathname === '/api/litellm-api/ccw-litellm/install' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async () => {
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
const path = await import('path');
|
||||
const fs = await import('fs');
|
||||
|
||||
// Try to find ccw-litellm package in distribution
|
||||
const possiblePaths = [
|
||||
path.join(initialPath, 'ccw-litellm'),
|
||||
path.join(initialPath, '..', 'ccw-litellm'),
|
||||
path.join(process.cwd(), 'ccw-litellm'),
|
||||
path.join(PACKAGE_ROOT, 'ccw-litellm'), // npm package internal path
|
||||
];
|
||||
|
||||
let packagePath = '';
|
||||
for (const p of possiblePaths) {
|
||||
const pyproject = path.join(p, 'pyproject.toml');
|
||||
if (fs.existsSync(pyproject)) {
|
||||
packagePath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!packagePath) {
|
||||
// Try pip install from PyPI as fallback
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('pip', ['install', 'ccw-litellm'], { shell: true, timeout: 300000 });
|
||||
let output = '';
|
||||
let error = '';
|
||||
proc.stdout?.on('data', (data) => { output += data.toString(); });
|
||||
proc.stderr?.on('data', (data) => { error += data.toString(); });
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
// Clear status cache after successful installation
|
||||
clearCcwLitellmStatusCache();
|
||||
resolve({ success: true, message: 'ccw-litellm installed from PyPI' });
|
||||
} else {
|
||||
resolve({ success: false, error: error || 'Installation failed' });
|
||||
}
|
||||
});
|
||||
proc.on('error', (err) => resolve({ success: false, error: err.message }));
|
||||
});
|
||||
}
|
||||
|
||||
// Install from local package
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('pip', ['install', '-e', packagePath], { shell: true, timeout: 300000 });
|
||||
let output = '';
|
||||
let error = '';
|
||||
proc.stdout?.on('data', (data) => { output += data.toString(); });
|
||||
proc.stderr?.on('data', (data) => { error += data.toString(); });
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
// Clear status cache after successful installation
|
||||
clearCcwLitellmStatusCache();
|
||||
|
||||
// Broadcast installation event
|
||||
broadcastToClients({
|
||||
type: 'CCW_LITELLM_INSTALLED',
|
||||
payload: { timestamp: new Date().toISOString() }
|
||||
});
|
||||
resolve({ success: true, message: 'ccw-litellm installed successfully', path: packagePath });
|
||||
} else {
|
||||
resolve({ success: false, error: error || output || 'Installation failed' });
|
||||
}
|
||||
});
|
||||
proc.on('error', (err) => resolve({ success: false, error: err.message }));
|
||||
});
|
||||
} catch (err) {
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/litellm-api/ccw-litellm/uninstall - Uninstall ccw-litellm package
|
||||
if (pathname === '/api/litellm-api/ccw-litellm/uninstall' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async () => {
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('pip', ['uninstall', '-y', 'ccw-litellm'], { shell: true, timeout: 120000 });
|
||||
let output = '';
|
||||
let error = '';
|
||||
proc.stdout?.on('data', (data) => { output += data.toString(); });
|
||||
proc.stderr?.on('data', (data) => { error += data.toString(); });
|
||||
proc.on('close', (code) => {
|
||||
// Clear status cache after uninstallation attempt
|
||||
clearCcwLitellmStatusCache();
|
||||
|
||||
if (code === 0) {
|
||||
broadcastToClients({
|
||||
type: 'CCW_LITELLM_UNINSTALLED',
|
||||
payload: { timestamp: new Date().toISOString() }
|
||||
});
|
||||
resolve({ success: true, message: 'ccw-litellm uninstalled successfully' });
|
||||
} else {
|
||||
// Check if package was not installed
|
||||
if (error.includes('not installed') || output.includes('not installed')) {
|
||||
resolve({ success: true, message: 'ccw-litellm was not installed' });
|
||||
} else {
|
||||
resolve({ success: false, error: error || output || 'Uninstallation failed' });
|
||||
}
|
||||
}
|
||||
});
|
||||
proc.on('error', (err) => resolve({ success: false, error: err.message }));
|
||||
});
|
||||
} catch (err) {
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
107
ccw/src/core/routes/litellm-routes.ts
Normal file
107
ccw/src/core/routes/litellm-routes.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* LiteLLM Routes Module
|
||||
* Handles all LiteLLM-related API endpoints
|
||||
*/
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { getLiteLLMClient, getLiteLLMStatus, checkLiteLLMAvailable } from '../../tools/litellm-client.js';
|
||||
|
||||
export interface RouteContext {
|
||||
pathname: string;
|
||||
url: URL;
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
initialPath: string;
|
||||
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
|
||||
broadcastToClients: (data: unknown) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle LiteLLM routes
|
||||
* @returns true if route was handled, false otherwise
|
||||
*/
|
||||
export async function handleLiteLLMRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
|
||||
|
||||
// API: LiteLLM Status - Check availability and version
|
||||
if (pathname === '/api/litellm/status') {
|
||||
try {
|
||||
const status = await getLiteLLMStatus();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(status));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ available: false, error: err.message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: LiteLLM Config - Get configuration
|
||||
if (pathname === '/api/litellm/config' && req.method === 'GET') {
|
||||
try {
|
||||
const client = getLiteLLMClient();
|
||||
const config = await client.getConfig();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(config));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err.message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: LiteLLM Embed - Generate embeddings
|
||||
if (pathname === '/api/litellm/embed' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { texts, model = 'default' } = body;
|
||||
|
||||
if (!texts || !Array.isArray(texts)) {
|
||||
return { error: 'texts array is required', status: 400 };
|
||||
}
|
||||
|
||||
if (texts.length === 0) {
|
||||
return { error: 'texts array cannot be empty', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
const client = getLiteLLMClient();
|
||||
const result = await client.embed(texts, model);
|
||||
return { success: true, ...result };
|
||||
} catch (err) {
|
||||
return { error: err.message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: LiteLLM Chat - Chat with LLM
|
||||
if (pathname === '/api/litellm/chat' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { message, messages, model = 'default' } = body;
|
||||
|
||||
// Support both single message and messages array
|
||||
if (!message && (!messages || !Array.isArray(messages))) {
|
||||
return { error: 'message or messages array is required', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
const client = getLiteLLMClient();
|
||||
|
||||
if (messages && Array.isArray(messages)) {
|
||||
// Multi-turn chat
|
||||
const result = await client.chatMessages(messages, model);
|
||||
return { success: true, ...result };
|
||||
} else {
|
||||
// Single message chat
|
||||
const content = await client.chat(message, model);
|
||||
return { success: true, content, model };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: err.message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1000,8 +1000,8 @@ function writeSettingsFile(filePath, settings) {
|
||||
* @returns {string}
|
||||
*/
|
||||
function getProjectSettingsPath(projectPath) {
|
||||
const normalizedPath = projectPath.replace(/\//g, '\\').replace(/^\\([a-zA-Z])\\/, '$1:\\');
|
||||
return join(normalizedPath, '.claude', 'settings.json');
|
||||
// path.join automatically handles cross-platform path separators
|
||||
return join(projectPath, '.claude', 'settings.json');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
|
||||
@@ -4,9 +4,56 @@
|
||||
* Aggregated status endpoint for faster dashboard loading
|
||||
*/
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { getCliToolsStatus } from '../../tools/cli-executor.js';
|
||||
import { checkVenvStatus, checkSemanticStatus } from '../../tools/codex-lens.js';
|
||||
|
||||
/**
|
||||
* Check CCW installation status
|
||||
* Verifies that required workflow files are installed in user's home directory
|
||||
*/
|
||||
function checkCcwInstallStatus(): {
|
||||
installed: boolean;
|
||||
workflowsInstalled: boolean;
|
||||
missingFiles: string[];
|
||||
installPath: string;
|
||||
} {
|
||||
const claudeDir = join(homedir(), '.claude');
|
||||
const workflowsDir = join(claudeDir, 'workflows');
|
||||
|
||||
// Required workflow files for full functionality
|
||||
const requiredFiles = [
|
||||
'chinese-response.md',
|
||||
'windows-platform.md',
|
||||
'cli-tools-usage.md',
|
||||
'coding-philosophy.md',
|
||||
'context-tools.md',
|
||||
'file-modification.md'
|
||||
];
|
||||
|
||||
const missingFiles: string[] = [];
|
||||
|
||||
// Check each required file
|
||||
for (const file of requiredFiles) {
|
||||
const filePath = join(workflowsDir, file);
|
||||
if (!existsSync(filePath)) {
|
||||
missingFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
const workflowsInstalled = existsSync(workflowsDir) && missingFiles.length === 0;
|
||||
const installed = existsSync(claudeDir) && workflowsInstalled;
|
||||
|
||||
return {
|
||||
installed,
|
||||
workflowsInstalled,
|
||||
missingFiles,
|
||||
installPath: claudeDir
|
||||
};
|
||||
}
|
||||
|
||||
export interface RouteContext {
|
||||
pathname: string;
|
||||
url: URL;
|
||||
@@ -27,6 +74,9 @@ export async function handleStatusRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
// API: Aggregated Status (all statuses in one call)
|
||||
if (pathname === '/api/status/all') {
|
||||
try {
|
||||
// Check CCW installation status (sync, fast)
|
||||
const ccwInstallStatus = checkCcwInstallStatus();
|
||||
|
||||
// Execute all status checks in parallel
|
||||
const [cliStatus, codexLensStatus, semanticStatus] = await Promise.all([
|
||||
getCliToolsStatus(),
|
||||
@@ -39,6 +89,7 @@ export async function handleStatusRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
cli: cliStatus,
|
||||
codexLens: codexLensStatus,
|
||||
semantic: semanticStatus,
|
||||
ccwInstall: ccwInstallStatus,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ import { handleSessionRoutes } from './routes/session-routes.js';
|
||||
import { handleCcwRoutes } from './routes/ccw-routes.js';
|
||||
import { handleClaudeRoutes } from './routes/claude-routes.js';
|
||||
import { handleHelpRoutes } from './routes/help-routes.js';
|
||||
import { handleLiteLLMRoutes } from './routes/litellm-routes.js';
|
||||
import { handleLiteLLMApiRoutes } from './routes/litellm-api-routes.js';
|
||||
|
||||
// Import WebSocket handling
|
||||
import { handleWebSocketUpgrade, broadcastToClients } from './websocket.js';
|
||||
@@ -83,7 +85,8 @@ const MODULE_CSS_FILES = [
|
||||
'27-graph-explorer.css',
|
||||
'28-mcp-manager.css',
|
||||
'29-help.css',
|
||||
'30-core-memory.css'
|
||||
'30-core-memory.css',
|
||||
'31-api-settings.css'
|
||||
];
|
||||
|
||||
// Modular JS files in dependency order
|
||||
@@ -137,6 +140,7 @@ const MODULE_FILES = [
|
||||
'views/skills-manager.js',
|
||||
'views/rules-manager.js',
|
||||
'views/claude-manager.js',
|
||||
'views/api-settings.js',
|
||||
'views/help.js',
|
||||
'main.js'
|
||||
];
|
||||
@@ -311,6 +315,16 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
if (await handleCodexLensRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// LiteLLM routes (/api/litellm/*)
|
||||
if (pathname.startsWith('/api/litellm/')) {
|
||||
if (await handleLiteLLMRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// LiteLLM API routes (/api/litellm-api/*)
|
||||
if (pathname.startsWith('/api/litellm-api/')) {
|
||||
if (await handleLiteLLMApiRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Graph routes (/api/graph/*)
|
||||
if (pathname.startsWith('/api/graph/')) {
|
||||
if (await handleGraphRoutes(routeContext)) return;
|
||||
|
||||
@@ -22,7 +22,7 @@ const ENV_PROJECT_ROOT = 'CCW_PROJECT_ROOT';
|
||||
const ENV_ALLOWED_DIRS = 'CCW_ALLOWED_DIRS';
|
||||
|
||||
// Default enabled tools (core set)
|
||||
const DEFAULT_TOOLS: string[] = ['write_file', 'edit_file', 'read_file', 'smart_search', 'core_memory'];
|
||||
const DEFAULT_TOOLS: string[] = ['write_file', 'edit_file', 'read_file', 'smart_search', 'core_memory', 'context_cache'];
|
||||
|
||||
/**
|
||||
* Get list of enabled tools from environment or defaults
|
||||
|
||||
@@ -170,6 +170,27 @@
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.cli-tool-badge-disabled {
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 600;
|
||||
padding: 0.125rem 0.375rem;
|
||||
background: hsl(38 92% 50% / 0.2);
|
||||
color: hsl(38 92% 50%);
|
||||
border-radius: 9999px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* Disabled tool card state */
|
||||
.cli-tool-card.disabled {
|
||||
opacity: 0.7;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.cli-tool-card.disabled .cli-tool-name {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.cli-tool-info {
|
||||
font-size: 0.6875rem;
|
||||
margin-bottom: 0.3125rem;
|
||||
@@ -773,6 +794,29 @@
|
||||
border-color: hsl(var(--destructive) / 0.5);
|
||||
}
|
||||
|
||||
/* Enable/Disable button variants */
|
||||
.btn-sm.btn-outline-success {
|
||||
background: transparent;
|
||||
border: 1px solid hsl(142 76% 36% / 0.4);
|
||||
color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.btn-sm.btn-outline-success:hover {
|
||||
background: hsl(142 76% 36% / 0.1);
|
||||
border-color: hsl(142 76% 36% / 0.6);
|
||||
}
|
||||
|
||||
.btn-sm.btn-outline-warning {
|
||||
background: transparent;
|
||||
border: 1px solid hsl(38 92% 50% / 0.4);
|
||||
color: hsl(38 92% 50%);
|
||||
}
|
||||
|
||||
.btn-sm.btn-outline-warning:hover {
|
||||
background: hsl(38 92% 50% / 0.1);
|
||||
border-color: hsl(38 92% 50% / 0.6);
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
|
||||
@@ -158,3 +158,37 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Code Index MCP Toggle Buttons */
|
||||
.code-mcp-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
background: transparent;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.code-mcp-btn:hover {
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
}
|
||||
|
||||
.code-mcp-btn.active,
|
||||
.code-mcp-btn[class*="bg-primary"] {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.code-mcp-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.125rem;
|
||||
}
|
||||
|
||||
|
||||
2265
ccw/src/templates/dashboard-css/31-api-settings.css
Normal file
2265
ccw/src/templates/dashboard-css/31-api-settings.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,9 @@ async function loadCliHistory(options = {}) {
|
||||
const { limit = cliHistoryLimit, tool = cliHistoryFilter, status = null } = options;
|
||||
|
||||
// Use history-native endpoint to get native session info
|
||||
let url = `/api/cli/history-native?path=${encodeURIComponent(projectPath)}&limit=${limit}`;
|
||||
// Use recursiveQueryEnabled setting (from cli-status.js) to control recursive query
|
||||
const recursive = typeof recursiveQueryEnabled !== 'undefined' ? recursiveQueryEnabled : true;
|
||||
let url = `/api/cli/history-native?path=${encodeURIComponent(projectPath)}&limit=${limit}&recursive=${recursive}`;
|
||||
if (tool) url += `&tool=${tool}`;
|
||||
if (status) url += `&status=${status}`;
|
||||
if (cliHistorySearch) url += `&search=${encodeURIComponent(cliHistorySearch)}`;
|
||||
@@ -33,9 +35,16 @@ async function loadCliHistory(options = {}) {
|
||||
}
|
||||
|
||||
// Load native session content for a specific execution
|
||||
async function loadNativeSessionContent(executionId) {
|
||||
async function loadNativeSessionContent(executionId, sourceDir) {
|
||||
try {
|
||||
const url = `/api/cli/native-session?path=${encodeURIComponent(projectPath)}&id=${encodeURIComponent(executionId)}`;
|
||||
// If sourceDir provided, use it to build the correct path
|
||||
// Check if sourceDir is absolute path (contains : or starts with /)
|
||||
let basePath = projectPath;
|
||||
if (sourceDir && sourceDir !== '.') {
|
||||
const isAbsolute = sourceDir.includes(':') || sourceDir.startsWith('/');
|
||||
basePath = isAbsolute ? sourceDir : projectPath + '/' + sourceDir;
|
||||
}
|
||||
const url = `/api/cli/native-session?path=${encodeURIComponent(basePath)}&id=${encodeURIComponent(executionId)}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
@@ -61,9 +70,12 @@ async function loadEnrichedConversation(executionId) {
|
||||
async function loadExecutionDetail(executionId, sourceDir) {
|
||||
try {
|
||||
// If sourceDir provided, use it to build the correct path
|
||||
const basePath = sourceDir && sourceDir !== '.'
|
||||
? projectPath + '/' + sourceDir
|
||||
: projectPath;
|
||||
// Check if sourceDir is absolute path (contains : or starts with /)
|
||||
let basePath = projectPath;
|
||||
if (sourceDir && sourceDir !== '.') {
|
||||
const isAbsolute = sourceDir.includes(':') || sourceDir.startsWith('/');
|
||||
basePath = isAbsolute ? sourceDir : projectPath + '/' + sourceDir;
|
||||
}
|
||||
const url = `/api/cli/execution?path=${encodeURIComponent(basePath)}&id=${encodeURIComponent(executionId)}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Execution not found');
|
||||
@@ -133,9 +145,13 @@ function renderCliHistory() {
|
||||
</span>`
|
||||
: '';
|
||||
|
||||
// Normalize and escape sourceDir for use in onclick
|
||||
// Convert backslashes to forward slashes to prevent JS escape issues in onclick
|
||||
const sourceDirEscaped = exec.sourceDir ? exec.sourceDir.replace(/\\/g, '/').replace(/'/g, "\\'") : '';
|
||||
|
||||
return `
|
||||
<div class="cli-history-item ${hasNative ? 'has-native' : ''}">
|
||||
<div class="cli-history-item-content" onclick="showExecutionDetail('${exec.id}')">
|
||||
<div class="cli-history-item-content" onclick="showExecutionDetail('${exec.id}', '${sourceDirEscaped}')">
|
||||
<div class="cli-history-item-header">
|
||||
<span class="cli-tool-tag cli-tool-${exec.tool}">${exec.tool.toUpperCase()}</span>
|
||||
<span class="cli-mode-tag">${exec.mode || 'analysis'}</span>
|
||||
@@ -148,20 +164,23 @@ function renderCliHistory() {
|
||||
<div class="cli-history-meta">
|
||||
<span><i data-lucide="clock" class="w-3 h-3"></i> ${timeAgo}</span>
|
||||
<span><i data-lucide="timer" class="w-3 h-3"></i> ${duration}</span>
|
||||
<span><i data-lucide="hash" class="w-3 h-3"></i> ${exec.id.split('-')[0]}</span>
|
||||
<span title="${exec.id}"><i data-lucide="hash" class="w-3 h-3"></i> ${exec.id.substring(0, 13)}...${exec.id.split('-').pop()}</span>
|
||||
${turnBadge}
|
||||
</div>
|
||||
</div>
|
||||
<div class="cli-history-actions">
|
||||
<button class="btn-icon" onclick="event.stopPropagation(); copyCliExecutionId('${exec.id}')" title="Copy ID">
|
||||
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
${hasNative ? `
|
||||
<button class="btn-icon" onclick="event.stopPropagation(); showNativeSessionDetail('${exec.id}')" title="View Native Session">
|
||||
<button class="btn-icon" onclick="event.stopPropagation(); showNativeSessionDetail('${exec.id}', '${sourceDirEscaped}')" title="View Native Session">
|
||||
<i data-lucide="file-json" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail('${exec.id}')" title="View Details">
|
||||
<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail('${exec.id}', '${sourceDirEscaped}')" title="View Details">
|
||||
<i data-lucide="eye" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution('${exec.id}')" title="Delete">
|
||||
<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution('${exec.id}', '${sourceDirEscaped}')" title="Delete">
|
||||
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -424,9 +443,12 @@ function confirmDeleteExecution(executionId, sourceDir) {
|
||||
async function deleteExecution(executionId, sourceDir) {
|
||||
try {
|
||||
// Build correct path - use sourceDir if provided for recursive items
|
||||
const basePath = sourceDir && sourceDir !== '.'
|
||||
? projectPath + '/' + sourceDir
|
||||
: projectPath;
|
||||
// Check if sourceDir is absolute path (contains : or starts with /)
|
||||
let basePath = projectPath;
|
||||
if (sourceDir && sourceDir !== '.') {
|
||||
const isAbsolute = sourceDir.includes(':') || sourceDir.startsWith('/');
|
||||
basePath = isAbsolute ? sourceDir : projectPath + '/' + sourceDir;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/cli/execution?path=${encodeURIComponent(basePath)}&id=${encodeURIComponent(executionId)}`, {
|
||||
method: 'DELETE'
|
||||
@@ -454,6 +476,18 @@ async function deleteExecution(executionId, sourceDir) {
|
||||
}
|
||||
|
||||
// ========== Copy Functions ==========
|
||||
async function copyCliExecutionId(executionId) {
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(executionId);
|
||||
showRefreshToast('ID copied: ' + executionId, 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy ID:', err);
|
||||
showRefreshToast('Failed to copy ID', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function copyExecutionPrompt(executionId) {
|
||||
const detail = await loadExecutionDetail(executionId);
|
||||
if (!detail) {
|
||||
@@ -650,9 +684,9 @@ async function copyConcatenatedPrompt(executionId) {
|
||||
/**
|
||||
* Show native session detail modal with full conversation content
|
||||
*/
|
||||
async function showNativeSessionDetail(executionId) {
|
||||
async function showNativeSessionDetail(executionId, sourceDir) {
|
||||
// Load native session content
|
||||
const nativeSession = await loadNativeSessionContent(executionId);
|
||||
const nativeSession = await loadNativeSessionContent(executionId, sourceDir);
|
||||
|
||||
if (!nativeSession) {
|
||||
showRefreshToast('Native session not found', 'error');
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
let cliToolStatus = { gemini: {}, qwen: {}, codex: {}, claude: {} };
|
||||
let codexLensStatus = { ready: false };
|
||||
let semanticStatus = { available: false };
|
||||
let ccwInstallStatus = { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
|
||||
let defaultCliTool = 'gemini';
|
||||
let promptConcatFormat = localStorage.getItem('ccw-prompt-format') || 'plain'; // plain, yaml, json
|
||||
let cliToolsConfig = {}; // CLI tools enable/disable config
|
||||
let apiEndpoints = []; // API endpoints from LiteLLM config
|
||||
|
||||
// Smart Context settings
|
||||
let smartContextEnabled = localStorage.getItem('ccw-smart-context') === 'true';
|
||||
@@ -18,6 +21,24 @@ let nativeResumeEnabled = localStorage.getItem('ccw-native-resume') !== 'false';
|
||||
// Recursive Query settings (for hierarchical storage aggregation)
|
||||
let recursiveQueryEnabled = localStorage.getItem('ccw-recursive-query') !== 'false'; // default true
|
||||
|
||||
// Code Index MCP provider (codexlens, ace, or none)
|
||||
let codeIndexMcpProvider = 'codexlens';
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
/**
|
||||
* Get the context-tools filename based on provider
|
||||
*/
|
||||
function getContextToolsFileName(provider) {
|
||||
switch (provider) {
|
||||
case 'ace':
|
||||
return 'context-tools-ace.md';
|
||||
case 'none':
|
||||
return 'context-tools-none.md';
|
||||
default:
|
||||
return 'context-tools.md';
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Initialization ==========
|
||||
function initCliStatus() {
|
||||
// Load all statuses in one call using aggregated endpoint
|
||||
@@ -38,10 +59,18 @@ async function loadAllStatuses() {
|
||||
cliToolStatus = data.cli || { gemini: {}, qwen: {}, codex: {}, claude: {} };
|
||||
codexLensStatus = data.codexLens || { ready: false };
|
||||
semanticStatus = data.semantic || { available: false };
|
||||
ccwInstallStatus = data.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
|
||||
|
||||
// Load CLI tools config and API endpoints
|
||||
await Promise.all([
|
||||
loadCliToolsConfig(),
|
||||
loadApiEndpoints()
|
||||
]);
|
||||
|
||||
// Update badges
|
||||
updateCliBadge();
|
||||
updateCodexLensBadge();
|
||||
updateCcwInstallBadge();
|
||||
|
||||
return data;
|
||||
} catch (err) {
|
||||
@@ -118,6 +147,54 @@ async function loadCodexLensStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load CodexLens dashboard data using aggregated endpoint (single API call)
|
||||
* This is optimized for the CodexLens Manager page initialization
|
||||
* @returns {Promise<object|null>} Dashboard init data or null on error
|
||||
*/
|
||||
async function loadCodexLensDashboardInit() {
|
||||
try {
|
||||
const response = await fetch('/api/codexlens/dashboard-init');
|
||||
if (!response.ok) throw new Error('Failed to load CodexLens dashboard init');
|
||||
const data = await response.json();
|
||||
|
||||
// Update status variables from aggregated response
|
||||
codexLensStatus = data.status || { ready: false };
|
||||
semanticStatus = data.semantic || { available: false };
|
||||
|
||||
// Expose to window for other modules
|
||||
if (!window.cliToolsStatus) {
|
||||
window.cliToolsStatus = {};
|
||||
}
|
||||
window.cliToolsStatus.codexlens = {
|
||||
installed: data.installed || false,
|
||||
version: data.status?.version || null,
|
||||
installedModels: [],
|
||||
config: data.config || {},
|
||||
semantic: data.semantic || {}
|
||||
};
|
||||
|
||||
// Store config globally for easy access
|
||||
window.codexLensConfig = data.config || {};
|
||||
window.codexLensStatusData = data.statusData || {};
|
||||
|
||||
// Update badges
|
||||
updateCodexLensBadge();
|
||||
|
||||
console.log('[CLI Status] CodexLens dashboard init loaded:', {
|
||||
installed: data.installed,
|
||||
version: data.status?.version,
|
||||
semanticAvailable: data.semantic?.available
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Failed to load CodexLens dashboard init:', err);
|
||||
// Fallback to individual calls
|
||||
return await loadCodexLensStatus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy: Load semantic status individually
|
||||
*/
|
||||
@@ -165,6 +242,72 @@ async function loadInstalledModels() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load CLI tools config from .claude/cli-tools.json (project or global fallback)
|
||||
*/
|
||||
async function loadCliToolsConfig() {
|
||||
try {
|
||||
const response = await fetch('/api/cli/tools-config');
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json();
|
||||
// Store full config and extract tools for backward compatibility
|
||||
cliToolsConfig = data.tools || {};
|
||||
window.claudeCliToolsConfig = data; // Full config available globally
|
||||
|
||||
// Load default tool from config
|
||||
if (data.defaultTool) {
|
||||
defaultCliTool = data.defaultTool;
|
||||
}
|
||||
|
||||
// Load Code Index MCP provider from config
|
||||
if (data.settings?.codeIndexMcp) {
|
||||
codeIndexMcpProvider = data.settings.codeIndexMcp;
|
||||
}
|
||||
|
||||
console.log('[CLI Config] Loaded from:', data._configInfo?.source || 'unknown', '| Default:', data.defaultTool, '| CodeIndexMCP:', codeIndexMcpProvider);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Failed to load CLI tools config:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update CLI tool enabled status
|
||||
*/
|
||||
async function updateCliToolEnabled(tool, enabled) {
|
||||
try {
|
||||
const response = await fetch('/api/cli/tools-config/' + tool, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: enabled })
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to update');
|
||||
showRefreshToast(tool + (enabled ? ' enabled' : ' disabled'), 'success');
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to update CLI tool:', err);
|
||||
showRefreshToast('Failed to update ' + tool, 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load API endpoints from LiteLLM config
|
||||
*/
|
||||
async function loadApiEndpoints() {
|
||||
try {
|
||||
const response = await fetch('/api/litellm-api/endpoints');
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json();
|
||||
apiEndpoints = data.endpoints || [];
|
||||
return apiEndpoints;
|
||||
} catch (err) {
|
||||
console.error('Failed to load API endpoints:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Badge Update ==========
|
||||
function updateCliBadge() {
|
||||
const badge = document.getElementById('badgeCliTools');
|
||||
@@ -187,6 +330,25 @@ function updateCodexLensBadge() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateCcwInstallBadge() {
|
||||
const badge = document.getElementById('badgeCcwInstall');
|
||||
if (badge) {
|
||||
if (ccwInstallStatus.installed) {
|
||||
badge.textContent = t('status.installed');
|
||||
badge.classList.add('text-success');
|
||||
badge.classList.remove('text-warning', 'text-destructive');
|
||||
} else if (ccwInstallStatus.workflowsInstalled === false) {
|
||||
badge.textContent = t('status.incomplete');
|
||||
badge.classList.add('text-warning');
|
||||
badge.classList.remove('text-success', 'text-destructive');
|
||||
} else {
|
||||
badge.textContent = t('status.notInstalled');
|
||||
badge.classList.add('text-destructive');
|
||||
badge.classList.remove('text-success', 'text-warning');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Rendering ==========
|
||||
function renderCliStatus() {
|
||||
const container = document.getElementById('cli-status-panel');
|
||||
@@ -212,25 +374,41 @@ function renderCliStatus() {
|
||||
const status = cliToolStatus[tool] || {};
|
||||
const isAvailable = status.available;
|
||||
const isDefault = defaultCliTool === tool;
|
||||
const config = cliToolsConfig[tool] || { enabled: true };
|
||||
const isEnabled = config.enabled !== false;
|
||||
const canSetDefault = isAvailable && isEnabled && !isDefault;
|
||||
|
||||
return `
|
||||
<div class="cli-tool-card tool-${tool} ${isAvailable ? 'available' : 'unavailable'}">
|
||||
<div class="cli-tool-card tool-${tool} ${isAvailable ? 'available' : 'unavailable'} ${!isEnabled ? 'disabled' : ''}">
|
||||
<div class="cli-tool-header">
|
||||
<span class="cli-tool-status ${isAvailable ? 'status-available' : 'status-unavailable'}"></span>
|
||||
<span class="cli-tool-status ${isAvailable && isEnabled ? 'status-available' : 'status-unavailable'}"></span>
|
||||
<span class="cli-tool-name">${tool.charAt(0).toUpperCase() + tool.slice(1)}</span>
|
||||
${isDefault ? '<span class="cli-tool-badge">Default</span>' : ''}
|
||||
${!isEnabled && isAvailable ? '<span class="cli-tool-badge-disabled">Disabled</span>' : ''}
|
||||
</div>
|
||||
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
|
||||
${toolDescriptions[tool]}
|
||||
</div>
|
||||
<div class="cli-tool-info mt-2">
|
||||
${isAvailable
|
||||
? `<span class="text-success flex items-center gap-1"><i data-lucide="check-circle" class="w-3 h-3"></i> Ready</span>`
|
||||
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
|
||||
}
|
||||
<div class="cli-tool-info mt-2 flex items-center justify-between">
|
||||
<div>
|
||||
${isAvailable
|
||||
? (isEnabled
|
||||
? `<span class="text-success flex items-center gap-1"><i data-lucide="check-circle" class="w-3 h-3"></i> Ready</span>`
|
||||
: `<span class="text-warning flex items-center gap-1"><i data-lucide="pause-circle" class="w-3 h-3"></i> Disabled</span>`)
|
||||
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="cli-tool-actions mt-3">
|
||||
${isAvailable && !isDefault
|
||||
<div class="cli-tool-actions mt-3 flex gap-2">
|
||||
${isAvailable ? (isEnabled
|
||||
? `<button class="btn-sm btn-outline-warning flex items-center gap-1" onclick="toggleCliTool('${tool}', false)">
|
||||
<i data-lucide="pause" class="w-3 h-3"></i> Disable
|
||||
</button>`
|
||||
: `<button class="btn-sm btn-outline-success flex items-center gap-1" onclick="toggleCliTool('${tool}', true)">
|
||||
<i data-lucide="play" class="w-3 h-3"></i> Enable
|
||||
</button>`
|
||||
) : ''}
|
||||
${canSetDefault
|
||||
? `<button class="btn-sm btn-outline flex items-center gap-1" onclick="setDefaultCliTool('${tool}')">
|
||||
<i data-lucide="star" class="w-3 h-3"></i> Set Default
|
||||
</button>`
|
||||
@@ -310,11 +488,75 @@ function renderCliStatus() {
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
// CCW Installation Status card (show warning if not fully installed)
|
||||
const ccwInstallHtml = !ccwInstallStatus.installed ? `
|
||||
<div class="cli-tool-card tool-ccw-install unavailable" style="border: 1px solid var(--warning); background: rgba(var(--warning-rgb), 0.05);">
|
||||
<div class="cli-tool-header">
|
||||
<span class="cli-tool-status status-unavailable" style="background: var(--warning);"></span>
|
||||
<span class="cli-tool-name">${t('status.ccwInstall')}</span>
|
||||
<span class="badge px-1.5 py-0.5 text-xs rounded bg-warning/20 text-warning">${t('status.required')}</span>
|
||||
</div>
|
||||
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
|
||||
${t('status.ccwInstallDesc')}
|
||||
</div>
|
||||
<div class="cli-tool-info mt-2">
|
||||
<span class="text-warning flex items-center gap-1">
|
||||
<i data-lucide="alert-triangle" class="w-3 h-3"></i>
|
||||
${ccwInstallStatus.missingFiles.length} ${t('status.filesMissing')}
|
||||
</span>
|
||||
</div>
|
||||
<div class="cli-tool-actions flex flex-col gap-2 mt-3">
|
||||
<div class="text-xs text-muted-foreground">
|
||||
<p class="mb-1">${t('status.missingFiles')}:</p>
|
||||
<ul class="list-disc list-inside text-xs opacity-70">
|
||||
${ccwInstallStatus.missingFiles.slice(0, 3).map(f => `<li>${f}</li>`).join('')}
|
||||
${ccwInstallStatus.missingFiles.length > 3 ? `<li>+${ccwInstallStatus.missingFiles.length - 3} more...</li>` : ''}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="bg-muted/50 rounded p-2 mt-2">
|
||||
<p class="text-xs font-medium mb-1">${t('status.runToFix')}:</p>
|
||||
<code class="text-xs bg-background px-2 py-1 rounded block">ccw install</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
// API Endpoints section
|
||||
const apiEndpointsHtml = apiEndpoints.length > 0 ? `
|
||||
<div class="cli-api-endpoints-section" style="margin-top: 1.5rem;">
|
||||
<div class="cli-section-header" style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;">
|
||||
<h4 style="display: flex; align-items: center; gap: 0.5rem; font-weight: 600; margin: 0;">
|
||||
<i data-lucide="link" class="w-4 h-4"></i> API Endpoints
|
||||
</h4>
|
||||
<span class="badge" style="padding: 0.125rem 0.5rem; font-size: 0.75rem; border-radius: 0.25rem; background: var(--muted); color: var(--muted-foreground);">${apiEndpoints.length}</span>
|
||||
</div>
|
||||
<div class="cli-endpoints-list" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 0.75rem;">
|
||||
${apiEndpoints.map(ep => `
|
||||
<div class="cli-endpoint-card ${ep.enabled ? 'available' : 'unavailable'}" style="padding: 0.75rem; border: 1px solid var(--border); border-radius: 0.5rem; background: var(--card);">
|
||||
<div class="cli-endpoint-header" style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<span class="cli-tool-status ${ep.enabled ? 'status-available' : 'status-unavailable'}" style="width: 8px; height: 8px; border-radius: 50%; background: ${ep.enabled ? 'var(--success)' : 'var(--muted-foreground)'}; flex-shrink: 0;"></span>
|
||||
<span class="cli-endpoint-id" style="font-weight: 500; font-size: 0.875rem;">${ep.id}</span>
|
||||
</div>
|
||||
<div class="cli-endpoint-info" style="margin-top: 0.25rem;">
|
||||
<span class="text-xs text-muted-foreground" style="font-size: 0.75rem; color: var(--muted-foreground);">${ep.model}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
// Config source info
|
||||
const configInfo = window.claudeCliToolsConfig?._configInfo || {};
|
||||
const configSourceLabel = configInfo.source === 'project' ? 'Project' : configInfo.source === 'global' ? 'Global' : 'Default';
|
||||
const configSourceClass = configInfo.source === 'project' ? 'text-success' : configInfo.source === 'global' ? 'text-primary' : 'text-muted-foreground';
|
||||
|
||||
// CLI Settings section
|
||||
const settingsHtml = `
|
||||
<div class="cli-settings-section">
|
||||
<div class="cli-settings-header">
|
||||
<h4><i data-lucide="settings" class="w-3.5 h-3.5"></i> Settings</h4>
|
||||
<span class="badge text-xs ${configSourceClass}" title="${configInfo.activePath || ''}">${configSourceLabel}</span>
|
||||
</div>
|
||||
<div class="cli-settings-grid">
|
||||
<div class="cli-setting-item">
|
||||
@@ -381,6 +623,47 @@ function renderCliStatus() {
|
||||
</div>
|
||||
<p class="cli-setting-desc">Maximum files to include in smart context</p>
|
||||
</div>
|
||||
<div class="cli-setting-item">
|
||||
<label class="cli-setting-label">
|
||||
<i data-lucide="hard-drive" class="w-3 h-3"></i>
|
||||
Cache Injection
|
||||
</label>
|
||||
<div class="cli-setting-control">
|
||||
<select class="cli-setting-select" onchange="setCacheInjectionMode(this.value)">
|
||||
<option value="auto" ${getCacheInjectionMode() === 'auto' ? 'selected' : ''}>Auto</option>
|
||||
<option value="manual" ${getCacheInjectionMode() === 'manual' ? 'selected' : ''}>Manual</option>
|
||||
<option value="disabled" ${getCacheInjectionMode() === 'disabled' ? 'selected' : ''}>Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="cli-setting-desc">Cache prefix/suffix injection mode for prompts</p>
|
||||
</div>
|
||||
<div class="cli-setting-item">
|
||||
<label class="cli-setting-label">
|
||||
<i data-lucide="search" class="w-3 h-3"></i>
|
||||
Code Index MCP
|
||||
</label>
|
||||
<div class="cli-setting-control">
|
||||
<div class="flex items-center bg-muted rounded-lg p-0.5">
|
||||
<button class="code-mcp-btn px-3 py-1.5 text-xs font-medium rounded-md transition-all ${codeIndexMcpProvider === 'codexlens' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
|
||||
onclick="setCodeIndexMcpProvider('codexlens')">
|
||||
CodexLens
|
||||
</button>
|
||||
<button class="code-mcp-btn px-3 py-1.5 text-xs font-medium rounded-md transition-all ${codeIndexMcpProvider === 'ace' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
|
||||
onclick="setCodeIndexMcpProvider('ace')">
|
||||
ACE
|
||||
</button>
|
||||
<button class="code-mcp-btn px-3 py-1.5 text-xs font-medium rounded-md transition-all ${codeIndexMcpProvider === 'none' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
|
||||
onclick="setCodeIndexMcpProvider('none')">
|
||||
None
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="cli-setting-desc">Code search provider (updates CLAUDE.md context-tools reference)</p>
|
||||
<p class="cli-setting-desc text-xs text-muted-foreground mt-1">
|
||||
<i data-lucide="file-text" class="w-3 h-3 inline-block mr-1"></i>
|
||||
Current: <code class="bg-muted px-1 rounded">${getContextToolsFileName(codeIndexMcpProvider)}</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -392,11 +675,13 @@ function renderCliStatus() {
|
||||
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
${ccwInstallHtml}
|
||||
<div class="cli-tools-grid">
|
||||
${toolsHtml}
|
||||
${codexLensHtml}
|
||||
${semanticHtml}
|
||||
</div>
|
||||
${apiEndpointsHtml}
|
||||
${settingsHtml}
|
||||
`;
|
||||
|
||||
@@ -408,7 +693,30 @@ function renderCliStatus() {
|
||||
|
||||
// ========== Actions ==========
|
||||
function setDefaultCliTool(tool) {
|
||||
// Validate: tool must be available and enabled
|
||||
const status = cliToolStatus[tool] || {};
|
||||
const config = cliToolsConfig[tool] || { enabled: true };
|
||||
|
||||
if (!status.available) {
|
||||
showRefreshToast(`Cannot set ${tool} as default: not installed`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.enabled === false) {
|
||||
showRefreshToast(`Cannot set ${tool} as default: tool is disabled`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
defaultCliTool = tool;
|
||||
// Save to config
|
||||
if (window.claudeCliToolsConfig) {
|
||||
window.claudeCliToolsConfig.defaultTool = tool;
|
||||
fetch('/api/cli/tools-config', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ defaultTool: tool })
|
||||
}).catch(err => console.error('Failed to save default tool:', err));
|
||||
}
|
||||
renderCliStatus();
|
||||
showRefreshToast(`Default CLI tool set to ${tool}`, 'success');
|
||||
}
|
||||
@@ -449,11 +757,94 @@ function setRecursiveQueryEnabled(enabled) {
|
||||
showRefreshToast(`Recursive Query ${enabled ? 'enabled' : 'disabled'}`, 'success');
|
||||
}
|
||||
|
||||
function getCacheInjectionMode() {
|
||||
if (window.claudeCliToolsConfig && window.claudeCliToolsConfig.settings) {
|
||||
return window.claudeCliToolsConfig.settings.cache?.injectionMode || 'auto';
|
||||
}
|
||||
return localStorage.getItem('ccw-cache-injection-mode') || 'auto';
|
||||
}
|
||||
|
||||
async function setCacheInjectionMode(mode) {
|
||||
try {
|
||||
const response = await fetch('/api/cli/tools-config/cache', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ injectionMode: mode })
|
||||
});
|
||||
if (response.ok) {
|
||||
localStorage.setItem('ccw-cache-injection-mode', mode);
|
||||
if (window.claudeCliToolsConfig) {
|
||||
window.claudeCliToolsConfig.settings.cache.injectionMode = mode;
|
||||
}
|
||||
showRefreshToast(`Cache injection mode set to ${mode}`, 'success');
|
||||
} else {
|
||||
showRefreshToast('Failed to update cache settings', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update cache settings:', err);
|
||||
showRefreshToast('Failed to update cache settings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function setCodeIndexMcpProvider(provider) {
|
||||
try {
|
||||
const response = await fetch('/api/cli/code-index-mcp', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ provider: provider })
|
||||
});
|
||||
if (response.ok) {
|
||||
codeIndexMcpProvider = provider;
|
||||
if (window.claudeCliToolsConfig && window.claudeCliToolsConfig.settings) {
|
||||
window.claudeCliToolsConfig.settings.codeIndexMcp = provider;
|
||||
}
|
||||
const providerName = provider === 'ace' ? 'ACE (Augment)' : provider === 'none' ? 'None (Built-in only)' : 'CodexLens';
|
||||
showRefreshToast(`Code Index MCP switched to ${providerName}`, 'success');
|
||||
// Re-render both CLI status and settings section
|
||||
if (typeof renderCliStatus === 'function') renderCliStatus();
|
||||
if (typeof renderCliSettingsSection === 'function') renderCliSettingsSection();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showRefreshToast(`Failed to switch Code Index MCP: ${data.error}`, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to switch Code Index MCP:', err);
|
||||
showRefreshToast('Failed to switch Code Index MCP', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAllCliStatus() {
|
||||
await loadAllStatuses();
|
||||
renderCliStatus();
|
||||
}
|
||||
|
||||
async function toggleCliTool(tool, enabled) {
|
||||
// If disabling the current default tool, switch to another available+enabled tool
|
||||
if (!enabled && defaultCliTool === tool) {
|
||||
const tools = ['gemini', 'qwen', 'codex', 'claude'];
|
||||
const newDefault = tools.find(t => {
|
||||
if (t === tool) return false;
|
||||
const status = cliToolStatus[t] || {};
|
||||
const config = cliToolsConfig[t] || { enabled: true };
|
||||
return status.available && config.enabled !== false;
|
||||
});
|
||||
|
||||
if (newDefault) {
|
||||
defaultCliTool = newDefault;
|
||||
if (window.claudeCliToolsConfig) {
|
||||
window.claudeCliToolsConfig.defaultTool = newDefault;
|
||||
}
|
||||
showRefreshToast(`Default tool switched to ${newDefault}`, 'info');
|
||||
} else {
|
||||
showRefreshToast(`Warning: No other enabled tool available for default`, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
await updateCliToolEnabled(tool, enabled);
|
||||
await loadAllStatuses();
|
||||
renderCliStatus();
|
||||
}
|
||||
|
||||
function installCodexLens() {
|
||||
openCodexLensInstallWizard();
|
||||
}
|
||||
|
||||
@@ -143,6 +143,18 @@ function initNavigation() {
|
||||
} else {
|
||||
console.error('renderCoreMemoryView not defined - please refresh the page');
|
||||
}
|
||||
} else if (currentView === 'codexlens-manager') {
|
||||
if (typeof renderCodexLensManager === 'function') {
|
||||
renderCodexLensManager();
|
||||
} else {
|
||||
console.error('renderCodexLensManager not defined - please refresh the page');
|
||||
}
|
||||
} else if (currentView === 'api-settings') {
|
||||
if (typeof renderApiSettings === 'function') {
|
||||
renderApiSettings();
|
||||
} else {
|
||||
console.error('renderApiSettings not defined - please refresh the page');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -183,6 +195,10 @@ function updateContentTitle() {
|
||||
titleEl.textContent = t('title.helpGuide');
|
||||
} else if (currentView === 'core-memory') {
|
||||
titleEl.textContent = t('title.coreMemory');
|
||||
} else if (currentView === 'codexlens-manager') {
|
||||
titleEl.textContent = t('title.codexLensManager');
|
||||
} else if (currentView === 'api-settings') {
|
||||
titleEl.textContent = t('title.apiSettings');
|
||||
} else if (currentView === 'liteTasks') {
|
||||
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
|
||||
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');
|
||||
|
||||
@@ -19,13 +19,18 @@ const i18n = {
|
||||
'common.delete': 'Delete',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.save': 'Save',
|
||||
'common.include': 'Include',
|
||||
'common.close': 'Close',
|
||||
'common.loading': 'Loading...',
|
||||
'common.error': 'Error',
|
||||
'common.success': 'Success',
|
||||
'common.deleteSuccess': 'Deleted successfully',
|
||||
'common.deleteFailed': 'Delete failed',
|
||||
'common.retry': 'Retry',
|
||||
'common.refresh': 'Refresh',
|
||||
'common.minutes': 'minutes',
|
||||
'common.enabled': 'Enabled',
|
||||
'common.disabled': 'Disabled',
|
||||
|
||||
// Header
|
||||
'header.project': 'Project:',
|
||||
@@ -41,6 +46,7 @@ const i18n = {
|
||||
'nav.explorer': 'Explorer',
|
||||
'nav.status': 'Status',
|
||||
'nav.history': 'History',
|
||||
'nav.codexLensManager': 'CodexLens',
|
||||
'nav.memory': 'Memory',
|
||||
'nav.contextMemory': 'Context',
|
||||
'nav.coreMemory': 'Core Memory',
|
||||
@@ -98,7 +104,8 @@ const i18n = {
|
||||
'title.hookManager': 'Hook Manager',
|
||||
'title.memoryModule': 'Memory Module',
|
||||
'title.promptHistory': 'Prompt History',
|
||||
|
||||
'title.codexLensManager': 'CodexLens Manager',
|
||||
|
||||
// Search
|
||||
'search.placeholder': 'Search...',
|
||||
|
||||
@@ -215,6 +222,7 @@ const i18n = {
|
||||
'cli.default': 'Default',
|
||||
'cli.install': 'Install',
|
||||
'cli.uninstall': 'Uninstall',
|
||||
'cli.openManager': 'Manager',
|
||||
'cli.initIndex': 'Init Index',
|
||||
'cli.geminiDesc': 'Google AI for code analysis',
|
||||
'cli.qwenDesc': 'Alibaba AI assistant',
|
||||
@@ -223,12 +231,19 @@ const i18n = {
|
||||
'cli.codexLensDescFull': 'Full-text code search engine',
|
||||
'cli.semanticDesc': 'AI-powered code understanding',
|
||||
'cli.semanticDescFull': 'Natural language code search',
|
||||
'cli.apiEndpoints': 'API Endpoints',
|
||||
'cli.configured': 'configured',
|
||||
'cli.addToCli': 'Add to CLI',
|
||||
'cli.enabled': 'Enabled',
|
||||
'cli.disabled': 'Disabled',
|
||||
|
||||
// CodexLens Configuration
|
||||
'codexlens.config': 'CodexLens Configuration',
|
||||
'codexlens.configDesc': 'Manage code indexing, semantic search, and embedding models',
|
||||
'codexlens.status': 'Status',
|
||||
'codexlens.installed': 'Installed',
|
||||
'codexlens.notInstalled': 'Not Installed',
|
||||
'codexlens.installFirst': 'Install CodexLens to access semantic search and model management features',
|
||||
'codexlens.indexes': 'Indexes',
|
||||
'codexlens.currentWorkspace': 'Current Workspace',
|
||||
'codexlens.indexStoragePath': 'Index Storage Path',
|
||||
@@ -237,6 +252,8 @@ const i18n = {
|
||||
'codexlens.newStoragePath': 'New Storage Path',
|
||||
'codexlens.pathPlaceholder': 'e.g., /path/to/indexes or ~/.codexlens/indexes',
|
||||
'codexlens.pathInfo': 'Supports ~ for home directory. Changes take effect immediately.',
|
||||
'codexlens.pathUnchanged': 'Path unchanged',
|
||||
'codexlens.pathEmpty': 'Path cannot be empty',
|
||||
'codexlens.migrationRequired': 'Migration Required',
|
||||
'codexlens.migrationWarning': 'After changing the path, existing indexes will need to be re-initialized for each workspace.',
|
||||
'codexlens.actions': 'Actions',
|
||||
@@ -244,6 +261,50 @@ const i18n = {
|
||||
'codexlens.cleanCurrentWorkspace': 'Clean Current Workspace',
|
||||
'codexlens.cleanAllIndexes': 'Clean All Indexes',
|
||||
'codexlens.installCodexLens': 'Install CodexLens',
|
||||
'codexlens.createIndex': 'Create Index',
|
||||
'codexlens.embeddingBackend': 'Embedding Backend',
|
||||
'codexlens.localFastembed': 'Local (FastEmbed)',
|
||||
'codexlens.apiLitellm': 'API (LiteLLM)',
|
||||
'codexlens.backendHint': 'Select local model or remote API endpoint',
|
||||
'codexlens.noApiModels': 'No API embedding models configured',
|
||||
'codexlens.embeddingModel': 'Embedding Model',
|
||||
'codexlens.modelHint': 'Select embedding model for vector search (models with ✓ are installed)',
|
||||
'codexlens.concurrency': 'API Concurrency',
|
||||
'codexlens.concurrencyHint': 'Number of parallel API calls. Higher values speed up indexing but may hit rate limits.',
|
||||
'codexlens.concurrencyCustom': 'Custom',
|
||||
'codexlens.rotation': 'Multi-Provider Rotation',
|
||||
'codexlens.rotationDesc': 'Aggregate multiple API providers and keys for parallel embedding generation',
|
||||
'codexlens.rotationEnabled': 'Enable Rotation',
|
||||
'codexlens.rotationStrategy': 'Rotation Strategy',
|
||||
'codexlens.strategyRoundRobin': 'Round Robin',
|
||||
'codexlens.strategyLatencyAware': 'Latency Aware',
|
||||
'codexlens.strategyWeightedRandom': 'Weighted Random',
|
||||
'codexlens.targetModel': 'Target Model',
|
||||
'codexlens.targetModelHint': 'Model name that all providers should support (e.g., qwen3-embedding)',
|
||||
'codexlens.cooldownSeconds': 'Cooldown (seconds)',
|
||||
'codexlens.cooldownHint': 'Default cooldown after rate limit (60s recommended)',
|
||||
'codexlens.rotationProviders': 'Rotation Providers',
|
||||
'codexlens.addProvider': 'Add Provider',
|
||||
'codexlens.noRotationProviders': 'No providers configured for rotation',
|
||||
'codexlens.providerWeight': 'Weight',
|
||||
'codexlens.maxConcurrentPerKey': 'Max Concurrent/Key',
|
||||
'codexlens.useAllKeys': 'Use All Keys',
|
||||
'codexlens.selectKeys': 'Select Keys',
|
||||
'codexlens.configureRotation': 'Configure Rotation',
|
||||
'codexlens.configureInApiSettings': 'Configure in API Settings',
|
||||
'codexlens.rotationSaved': 'Rotation config saved successfully',
|
||||
'codexlens.endpointsSynced': 'endpoints synced to CodexLens',
|
||||
'codexlens.syncFailed': 'Sync failed',
|
||||
'codexlens.rotationDeleted': 'Rotation config deleted',
|
||||
'codexlens.totalEndpoints': 'Total Endpoints',
|
||||
'codexlens.fullIndex': 'Full',
|
||||
'codexlens.vectorIndex': 'Vector',
|
||||
'codexlens.ftsIndex': 'FTS',
|
||||
'codexlens.fullIndexDesc': 'FTS + Semantic search (recommended)',
|
||||
'codexlens.vectorIndexDesc': 'Semantic search with embeddings only',
|
||||
'codexlens.ftsIndexDesc': 'Fast full-text search only',
|
||||
'codexlens.indexTypeHint': 'Full index includes FTS + semantic search. FTS only is faster but without AI-powered search.',
|
||||
'codexlens.maintenance': 'Maintenance',
|
||||
'codexlens.testSearch': 'Test Search',
|
||||
'codexlens.testFunctionality': 'test CodexLens functionality',
|
||||
'codexlens.textSearch': 'Text Search',
|
||||
@@ -257,6 +318,9 @@ const i18n = {
|
||||
'codexlens.runSearch': 'Run Search',
|
||||
'codexlens.results': 'Results',
|
||||
'codexlens.resultsCount': 'results',
|
||||
'codexlens.resultLimit': 'Limit',
|
||||
'codexlens.contentLength': 'Content Length',
|
||||
'codexlens.extraFiles': 'Extra Files',
|
||||
'codexlens.saveConfig': 'Save Configuration',
|
||||
'codexlens.searching': 'Searching...',
|
||||
'codexlens.searchCompleted': 'Search completed',
|
||||
@@ -291,6 +355,17 @@ const i18n = {
|
||||
'codexlens.cudaModeDesc': 'NVIDIA GPU (requires CUDA Toolkit)',
|
||||
'common.recommended': 'Recommended',
|
||||
'common.unavailable': 'Unavailable',
|
||||
'common.auto': 'Auto',
|
||||
|
||||
// GPU Device Selection
|
||||
'codexlens.selectGpuDevice': 'Select GPU Device',
|
||||
'codexlens.discrete': 'Discrete',
|
||||
'codexlens.integrated': 'Integrated',
|
||||
'codexlens.selectingGpu': 'Selecting GPU...',
|
||||
'codexlens.gpuSelected': 'GPU selected',
|
||||
'codexlens.resettingGpu': 'Resetting GPU selection...',
|
||||
'codexlens.gpuReset': 'GPU selection reset to auto',
|
||||
'codexlens.resetToAuto': 'Reset to Auto',
|
||||
'codexlens.modelManagement': 'Model Management',
|
||||
'codexlens.loadingModels': 'Loading models...',
|
||||
'codexlens.downloadModel': 'Download',
|
||||
@@ -342,6 +417,8 @@ const i18n = {
|
||||
'codexlens.indexComplete': 'Index complete',
|
||||
'codexlens.indexSuccess': 'Index created successfully',
|
||||
'codexlens.indexFailed': 'Indexing failed',
|
||||
'codexlens.embeddingsFailed': 'Embeddings generation failed',
|
||||
'codexlens.ftsSuccessEmbeddingsFailed': 'FTS index created, but embeddings failed',
|
||||
|
||||
// CodexLens Install
|
||||
'codexlens.installDesc': 'Python-based code indexing engine',
|
||||
@@ -444,6 +521,19 @@ const i18n = {
|
||||
'lang.windowsDisableSuccess': 'Windows platform guidelines disabled',
|
||||
'lang.windowsEnableFailed': 'Failed to enable Windows platform guidelines',
|
||||
'lang.windowsDisableFailed': 'Failed to disable Windows platform guidelines',
|
||||
'lang.installRequired': 'Run "ccw install" to enable this feature',
|
||||
|
||||
// CCW Installation Status
|
||||
'status.installed': 'Installed',
|
||||
'status.incomplete': 'Incomplete',
|
||||
'status.notInstalled': 'Not Installed',
|
||||
'status.ccwInstall': 'CCW Workflows',
|
||||
'status.ccwInstallDesc': 'Required workflow files for full functionality',
|
||||
'status.required': 'Required',
|
||||
'status.filesMissing': 'files missing',
|
||||
'status.missingFiles': 'Missing files',
|
||||
'status.runToFix': 'Run to fix',
|
||||
|
||||
'cli.promptFormat': 'Prompt Format',
|
||||
'cli.promptFormatDesc': 'Format for multi-turn conversation concatenation',
|
||||
'cli.storageBackend': 'Storage Backend',
|
||||
@@ -456,7 +546,9 @@ const i18n = {
|
||||
'cli.recursiveQueryDesc': 'Aggregate CLI history and memory data from parent and child projects',
|
||||
'cli.maxContextFiles': 'Max Context Files',
|
||||
'cli.maxContextFilesDesc': 'Maximum files to include in smart context',
|
||||
|
||||
'cli.codeIndexMcp': 'Code Index MCP',
|
||||
'cli.codeIndexMcpDesc': 'Code search provider (updates CLAUDE.md context-tools reference)',
|
||||
|
||||
// CCW Install
|
||||
'ccw.install': 'CCW Install',
|
||||
'ccw.installations': 'installation',
|
||||
@@ -1289,6 +1381,206 @@ const i18n = {
|
||||
'claude.unsupportedFileType': 'Unsupported file type',
|
||||
'claude.loadFileError': 'Failed to load file',
|
||||
|
||||
|
||||
// API Settings
|
||||
'nav.apiSettings': 'API Settings',
|
||||
'title.apiSettings': 'API Settings',
|
||||
'apiSettings.providers': 'Providers',
|
||||
'apiSettings.customEndpoints': 'Custom Endpoints',
|
||||
'apiSettings.cacheSettings': 'Cache Settings',
|
||||
'apiSettings.addProvider': 'Add Provider',
|
||||
'apiSettings.editProvider': 'Edit Provider',
|
||||
'apiSettings.deleteProvider': 'Delete Provider',
|
||||
'apiSettings.addEndpoint': 'Add Endpoint',
|
||||
'apiSettings.editEndpoint': 'Edit Endpoint',
|
||||
'apiSettings.deleteEndpoint': 'Delete Endpoint',
|
||||
'apiSettings.providerType': 'Provider Type',
|
||||
'apiSettings.apiFormat': 'API Format',
|
||||
'apiSettings.compatible': 'Compatible',
|
||||
'apiSettings.customFormat': 'Custom Format',
|
||||
'apiSettings.apiFormatHint': 'Most providers (DeepSeek, Ollama, etc.) use OpenAI-compatible format',
|
||||
'apiSettings.displayName': 'Display Name',
|
||||
'apiSettings.apiKey': 'API Key',
|
||||
'apiSettings.apiBaseUrl': 'API Base URL',
|
||||
'apiSettings.useEnvVar': 'Use environment variable',
|
||||
'apiSettings.enableProvider': 'Enable provider',
|
||||
'apiSettings.advancedSettings': 'Advanced Settings',
|
||||
'apiSettings.basicInfo': 'Basic Info',
|
||||
'apiSettings.endpointSettings': 'Endpoint Settings',
|
||||
'apiSettings.timeout': 'Timeout (seconds)',
|
||||
'apiSettings.seconds': 'seconds',
|
||||
'apiSettings.timeoutHint': 'Request timeout in seconds (default: 300)',
|
||||
'apiSettings.maxRetries': 'Max Retries',
|
||||
'apiSettings.maxRetriesHint': 'Maximum retry attempts on failure',
|
||||
'apiSettings.organization': 'Organization ID',
|
||||
'apiSettings.organizationHint': 'OpenAI organization ID (org-...)',
|
||||
'apiSettings.apiVersion': 'API Version',
|
||||
'apiSettings.apiVersionHint': 'Azure API version (e.g., 2024-02-01)',
|
||||
'apiSettings.rpm': 'RPM Limit',
|
||||
'apiSettings.tpm': 'TPM Limit',
|
||||
'apiSettings.unlimited': 'Unlimited',
|
||||
'apiSettings.proxy': 'Proxy Server',
|
||||
'apiSettings.proxyHint': 'HTTP proxy server URL',
|
||||
'apiSettings.customHeaders': 'Custom Headers',
|
||||
'apiSettings.customHeadersHint': 'JSON object with custom HTTP headers',
|
||||
'apiSettings.invalidJsonHeaders': 'Invalid JSON in custom headers',
|
||||
'apiSettings.searchProviders': 'Search providers...',
|
||||
'apiSettings.selectProvider': 'Select a Provider',
|
||||
'apiSettings.selectProviderHint': 'Select a provider from the list to view and manage its settings',
|
||||
'apiSettings.noProvidersFound': 'No providers found',
|
||||
'apiSettings.llmModels': 'LLM Models',
|
||||
'apiSettings.embeddingModels': 'Embedding Models',
|
||||
'apiSettings.manageModels': 'Manage',
|
||||
'apiSettings.addModel': 'Add Model',
|
||||
'apiSettings.multiKeySettings': 'Multi-Key Settings',
|
||||
'apiSettings.noModels': 'No models configured',
|
||||
'apiSettings.previewModel': 'Preview',
|
||||
'apiSettings.modelSettings': 'Model Settings',
|
||||
'apiSettings.deleteModel': 'Delete Model',
|
||||
'apiSettings.endpointPreview': 'Endpoint Preview',
|
||||
'apiSettings.modelBaseUrlOverride': 'Base URL Override',
|
||||
'apiSettings.modelBaseUrlHint': 'Override the provider base URL for this specific model (leave empty to use provider default)',
|
||||
'apiSettings.providerUpdated': 'Provider updated',
|
||||
'apiSettings.syncToCodexLens': 'Sync to CodexLens',
|
||||
'apiSettings.configSynced': 'Config synced to CodexLens',
|
||||
'apiSettings.sdkAutoAppends': 'SDK auto-appends',
|
||||
'apiSettings.preview': 'Preview',
|
||||
'apiSettings.used': 'used',
|
||||
'apiSettings.total': 'total',
|
||||
'apiSettings.testConnection': 'Test Connection',
|
||||
'apiSettings.endpointId': 'Endpoint ID',
|
||||
'apiSettings.endpointIdHint': 'Usage: ccw cli -p "..." --model <endpoint-id>',
|
||||
'apiSettings.endpoints': 'Endpoints',
|
||||
'apiSettings.addEndpointHint': 'Create custom endpoint aliases for CLI usage',
|
||||
'apiSettings.endpointModel': 'Model',
|
||||
'apiSettings.selectEndpoint': 'Select an endpoint',
|
||||
'apiSettings.selectEndpointHint': 'Choose an endpoint from the list to view or edit its settings',
|
||||
'apiSettings.provider': 'Provider',
|
||||
'apiSettings.model': 'Model',
|
||||
'apiSettings.selectModel': 'Select model',
|
||||
'apiSettings.noModelsConfigured': 'No models configured for this provider',
|
||||
'apiSettings.cacheStrategy': 'Cache Strategy',
|
||||
'apiSettings.enableContextCaching': 'Enable Context Caching',
|
||||
'apiSettings.cacheTTL': 'TTL (minutes)',
|
||||
'apiSettings.cacheMaxSize': 'Max Size (KB)',
|
||||
'apiSettings.autoCachePatterns': 'Auto-cache file patterns',
|
||||
'apiSettings.enableGlobalCaching': 'Enable Global Caching',
|
||||
'apiSettings.cacheUsed': 'Used',
|
||||
'apiSettings.cacheEntries': 'Entries',
|
||||
'apiSettings.clearCache': 'Clear Cache',
|
||||
'apiSettings.noProviders': 'No providers configured',
|
||||
'apiSettings.noEndpoints': 'No endpoints configured',
|
||||
'apiSettings.enabled': 'Enabled',
|
||||
'apiSettings.disabled': 'Disabled',
|
||||
'apiSettings.cacheEnabled': 'Cache Enabled',
|
||||
'apiSettings.cacheDisabled': 'Cache Disabled',
|
||||
'apiSettings.providerSaved': 'Provider saved successfully',
|
||||
'apiSettings.providerDeleted': 'Provider deleted successfully',
|
||||
'apiSettings.apiBaseUpdated': 'API Base URL updated successfully',
|
||||
'apiSettings.endpointSaved': 'Endpoint saved successfully',
|
||||
'apiSettings.endpointDeleted': 'Endpoint deleted successfully',
|
||||
'apiSettings.cacheCleared': 'Cache cleared successfully',
|
||||
'apiSettings.cacheSettingsUpdated': 'Cache settings updated',
|
||||
'apiSettings.embeddingPool': 'Embedding Pool',
|
||||
'apiSettings.embeddingPoolDesc': 'Auto-rotate between providers with same model',
|
||||
'apiSettings.targetModel': 'Target Model',
|
||||
'apiSettings.discoveredProviders': 'Discovered Providers',
|
||||
'apiSettings.autoDiscover': 'Auto-discover providers',
|
||||
'apiSettings.excludeProvider': 'Exclude',
|
||||
'apiSettings.defaultCooldown': 'Cooldown (seconds)',
|
||||
'apiSettings.defaultConcurrent': 'Concurrent per key',
|
||||
'apiSettings.poolEnabled': 'Enable Embedding Pool',
|
||||
'apiSettings.noProvidersFound': 'No providers found for this model',
|
||||
'apiSettings.poolSaved': 'Embedding pool config saved',
|
||||
'apiSettings.strategy': 'Strategy',
|
||||
'apiSettings.providerKeys': 'keys',
|
||||
'apiSettings.selectTargetModel': 'Select target model',
|
||||
'apiSettings.confirmDeleteProvider': 'Are you sure you want to delete this provider?',
|
||||
'apiSettings.confirmDeleteEndpoint': 'Are you sure you want to delete this endpoint?',
|
||||
'apiSettings.confirmClearCache': 'Are you sure you want to clear the cache?',
|
||||
'apiSettings.connectionSuccess': 'Connection successful',
|
||||
'apiSettings.connectionFailed': 'Connection failed',
|
||||
'apiSettings.saveProviderFirst': 'Please save the provider first',
|
||||
'apiSettings.addProviderFirst': 'Please add a provider first',
|
||||
'apiSettings.failedToLoad': 'Failed to load API settings',
|
||||
'apiSettings.toggleVisibility': 'Toggle visibility',
|
||||
'apiSettings.noProvidersHint': 'Add an API provider to get started',
|
||||
'apiSettings.noEndpointsHint': 'Create custom endpoints for quick access to models',
|
||||
'apiSettings.cache': 'Cache',
|
||||
'apiSettings.off': 'Off',
|
||||
'apiSettings.used': 'used',
|
||||
'apiSettings.total': 'total',
|
||||
'apiSettings.cacheUsage': 'Usage',
|
||||
'apiSettings.cacheSize': 'Size',
|
||||
'apiSettings.endpointsDescription': 'Manage custom API endpoints for quick model access',
|
||||
'apiSettings.totalEndpoints': 'Total Endpoints',
|
||||
'apiSettings.cachedEndpoints': 'Cached Endpoints',
|
||||
'apiSettings.cacheTabHint': 'Configure global cache settings and view statistics in the main panel',
|
||||
'apiSettings.cacheDescription': 'Manage response caching to improve performance and reduce costs',
|
||||
'apiSettings.cachedEntries': 'Cached Entries',
|
||||
'apiSettings.storageUsed': 'Storage Used',
|
||||
'apiSettings.cacheActions': 'Cache Actions',
|
||||
'apiSettings.cacheStatistics': 'Cache Statistics',
|
||||
'apiSettings.globalCache': 'Global Cache',
|
||||
|
||||
// Multi-key management
|
||||
'apiSettings.apiKeys': 'API Keys',
|
||||
'apiSettings.addKey': 'Add Key',
|
||||
'apiSettings.keyLabel': 'Label',
|
||||
'apiSettings.keyValue': 'API Key',
|
||||
'apiSettings.keyWeight': 'Weight',
|
||||
'apiSettings.removeKey': 'Remove',
|
||||
'apiSettings.noKeys': 'No API keys configured',
|
||||
'apiSettings.primaryKey': 'Primary Key',
|
||||
|
||||
// Routing strategy
|
||||
'apiSettings.routingStrategy': 'Routing Strategy',
|
||||
'apiSettings.simpleShuffleRouting': 'Simple Shuffle (Random)',
|
||||
'apiSettings.weightedRouting': 'Weighted Distribution',
|
||||
'apiSettings.latencyRouting': 'Latency-Based',
|
||||
'apiSettings.costRouting': 'Cost-Based',
|
||||
'apiSettings.leastBusyRouting': 'Least Busy',
|
||||
'apiSettings.routingHint': 'How to distribute requests across multiple API keys',
|
||||
|
||||
// Health check
|
||||
'apiSettings.healthCheck': 'Health Check',
|
||||
'apiSettings.enableHealthCheck': 'Enable Health Check',
|
||||
'apiSettings.healthInterval': 'Check Interval (seconds)',
|
||||
'apiSettings.healthCooldown': 'Cooldown (seconds)',
|
||||
'apiSettings.failureThreshold': 'Failure Threshold',
|
||||
'apiSettings.healthStatus': 'Status',
|
||||
'apiSettings.healthy': 'Healthy',
|
||||
'apiSettings.unhealthy': 'Unhealthy',
|
||||
'apiSettings.unknown': 'Unknown',
|
||||
'apiSettings.lastCheck': 'Last Check',
|
||||
'apiSettings.testKey': 'Test Key',
|
||||
'apiSettings.testingKey': 'Testing...',
|
||||
'apiSettings.keyValid': 'Key is valid',
|
||||
'apiSettings.keyInvalid': 'Key is invalid',
|
||||
|
||||
// Embedding models
|
||||
'apiSettings.embeddingDimensions': 'Dimensions',
|
||||
'apiSettings.embeddingMaxTokens': 'Max Tokens',
|
||||
'apiSettings.selectEmbeddingModel': 'Select Embedding Model',
|
||||
|
||||
// Model modal
|
||||
'apiSettings.addLlmModel': 'Add LLM Model',
|
||||
'apiSettings.addEmbeddingModel': 'Add Embedding Model',
|
||||
'apiSettings.modelId': 'Model ID',
|
||||
'apiSettings.modelName': 'Display Name',
|
||||
'apiSettings.modelSeries': 'Series',
|
||||
'apiSettings.selectFromPresets': 'Select from Presets',
|
||||
'apiSettings.customModel': 'Custom Model',
|
||||
'apiSettings.capabilities': 'Capabilities',
|
||||
'apiSettings.streaming': 'Streaming',
|
||||
'apiSettings.functionCalling': 'Function Calling',
|
||||
'apiSettings.vision': 'Vision',
|
||||
'apiSettings.contextWindow': 'Context Window',
|
||||
'apiSettings.description': 'Description',
|
||||
'apiSettings.optional': 'Optional',
|
||||
'apiSettings.modelIdExists': 'Model ID already exists',
|
||||
'apiSettings.useModelTreeToManage': 'Use the model tree to manage individual models',
|
||||
|
||||
// Common
|
||||
'common.cancel': 'Cancel',
|
||||
'common.optional': '(Optional)',
|
||||
@@ -1312,6 +1604,7 @@ const i18n = {
|
||||
'common.saveFailed': 'Failed to save',
|
||||
'common.unknownError': 'Unknown error',
|
||||
'common.exception': 'Exception',
|
||||
'common.status': 'Status',
|
||||
|
||||
// Core Memory
|
||||
'title.coreMemory': 'Core Memory',
|
||||
@@ -1435,13 +1728,18 @@ const i18n = {
|
||||
'common.delete': '删除',
|
||||
'common.cancel': '取消',
|
||||
'common.save': '保存',
|
||||
'common.include': '包含',
|
||||
'common.close': '关闭',
|
||||
'common.loading': '加载中...',
|
||||
'common.error': '错误',
|
||||
'common.success': '成功',
|
||||
'common.deleteSuccess': '删除成功',
|
||||
'common.deleteFailed': '删除失败',
|
||||
'common.retry': '重试',
|
||||
'common.refresh': '刷新',
|
||||
'common.minutes': '分钟',
|
||||
'common.enabled': '已启用',
|
||||
'common.disabled': '已禁用',
|
||||
|
||||
// Header
|
||||
'header.project': '项目:',
|
||||
@@ -1457,6 +1755,7 @@ const i18n = {
|
||||
'nav.explorer': '文件浏览器',
|
||||
'nav.status': '状态',
|
||||
'nav.history': '历史',
|
||||
'nav.codexLensManager': 'CodexLens',
|
||||
'nav.memory': '记忆',
|
||||
'nav.contextMemory': '活动',
|
||||
'nav.coreMemory': '核心记忆',
|
||||
@@ -1514,6 +1813,7 @@ const i18n = {
|
||||
'title.hookManager': '钩子管理',
|
||||
'title.memoryModule': '记忆模块',
|
||||
'title.promptHistory': '提示历史',
|
||||
'title.codexLensManager': 'CodexLens 管理',
|
||||
|
||||
// Search
|
||||
'search.placeholder': '搜索...',
|
||||
@@ -1631,6 +1931,7 @@ const i18n = {
|
||||
'cli.default': '默认',
|
||||
'cli.install': '安装',
|
||||
'cli.uninstall': '卸载',
|
||||
'cli.openManager': '管理',
|
||||
'cli.initIndex': '初始化索引',
|
||||
'cli.geminiDesc': 'Google AI 代码分析',
|
||||
'cli.qwenDesc': '阿里通义 AI 助手',
|
||||
@@ -1639,12 +1940,19 @@ const i18n = {
|
||||
'cli.codexLensDescFull': '全文代码搜索引擎',
|
||||
'cli.semanticDesc': 'AI 驱动的代码理解',
|
||||
'cli.semanticDescFull': '自然语言代码搜索',
|
||||
'cli.apiEndpoints': 'API 端点',
|
||||
'cli.configured': '已配置',
|
||||
'cli.addToCli': '添加到 CLI',
|
||||
'cli.enabled': '已启用',
|
||||
'cli.disabled': '已禁用',
|
||||
|
||||
// CodexLens 配置
|
||||
'codexlens.config': 'CodexLens 配置',
|
||||
'codexlens.configDesc': '管理代码索引、语义搜索和嵌入模型',
|
||||
'codexlens.status': '状态',
|
||||
'codexlens.installed': '已安装',
|
||||
'codexlens.notInstalled': '未安装',
|
||||
'codexlens.installFirst': '安装 CodexLens 以访问语义搜索和模型管理功能',
|
||||
'codexlens.indexes': '索引',
|
||||
'codexlens.currentWorkspace': '当前工作区',
|
||||
'codexlens.indexStoragePath': '索引存储路径',
|
||||
@@ -1653,6 +1961,8 @@ const i18n = {
|
||||
'codexlens.newStoragePath': '新存储路径',
|
||||
'codexlens.pathPlaceholder': '例如:/path/to/indexes 或 ~/.codexlens/indexes',
|
||||
'codexlens.pathInfo': '支持 ~ 表示用户目录。更改立即生效。',
|
||||
'codexlens.pathUnchanged': '路径未变更',
|
||||
'codexlens.pathEmpty': '路径不能为空',
|
||||
'codexlens.migrationRequired': '需要迁移',
|
||||
'codexlens.migrationWarning': '更改路径后,需要为每个工作区重新初始化索引。',
|
||||
'codexlens.actions': '操作',
|
||||
@@ -1660,6 +1970,50 @@ const i18n = {
|
||||
'codexlens.cleanCurrentWorkspace': '清理当前工作空间',
|
||||
'codexlens.cleanAllIndexes': '清理所有索引',
|
||||
'codexlens.installCodexLens': '安装 CodexLens',
|
||||
'codexlens.createIndex': '创建索引',
|
||||
'codexlens.embeddingBackend': '嵌入后端',
|
||||
'codexlens.localFastembed': '本地 (FastEmbed)',
|
||||
'codexlens.apiLitellm': 'API (LiteLLM)',
|
||||
'codexlens.backendHint': '选择本地模型或远程 API 端点',
|
||||
'codexlens.noApiModels': '未配置 API 嵌入模型',
|
||||
'codexlens.embeddingModel': '嵌入模型',
|
||||
'codexlens.modelHint': '选择向量搜索的嵌入模型(带 ✓ 的已安装)',
|
||||
'codexlens.concurrency': 'API 并发数',
|
||||
'codexlens.concurrencyHint': '并行 API 调用数量。较高的值可加速索引但可能触发速率限制。',
|
||||
'codexlens.concurrencyCustom': '自定义',
|
||||
'codexlens.rotation': '多供应商轮训',
|
||||
'codexlens.rotationDesc': '聚合多个 API 供应商和密钥进行并行嵌入生成',
|
||||
'codexlens.rotationEnabled': '启用轮训',
|
||||
'codexlens.rotationStrategy': '轮训策略',
|
||||
'codexlens.strategyRoundRobin': '轮询',
|
||||
'codexlens.strategyLatencyAware': '延迟感知',
|
||||
'codexlens.strategyWeightedRandom': '加权随机',
|
||||
'codexlens.targetModel': '目标模型',
|
||||
'codexlens.targetModelHint': '所有供应商应支持的模型名称(例如 qwen3-embedding)',
|
||||
'codexlens.cooldownSeconds': '冷却时间(秒)',
|
||||
'codexlens.cooldownHint': '速率限制后的默认冷却时间(推荐 60 秒)',
|
||||
'codexlens.rotationProviders': '轮训供应商',
|
||||
'codexlens.addProvider': '添加供应商',
|
||||
'codexlens.noRotationProviders': '未配置轮训供应商',
|
||||
'codexlens.providerWeight': '权重',
|
||||
'codexlens.maxConcurrentPerKey': '每密钥最大并发',
|
||||
'codexlens.useAllKeys': '使用所有密钥',
|
||||
'codexlens.selectKeys': '选择密钥',
|
||||
'codexlens.configureRotation': '配置轮训',
|
||||
'codexlens.configureInApiSettings': '在 API 设置中配置',
|
||||
'codexlens.rotationSaved': '轮训配置保存成功',
|
||||
'codexlens.endpointsSynced': '个端点已同步到 CodexLens',
|
||||
'codexlens.syncFailed': '同步失败',
|
||||
'codexlens.rotationDeleted': '轮训配置已删除',
|
||||
'codexlens.totalEndpoints': '总端点数',
|
||||
'codexlens.fullIndex': '全部',
|
||||
'codexlens.vectorIndex': '向量',
|
||||
'codexlens.ftsIndex': 'FTS',
|
||||
'codexlens.fullIndexDesc': 'FTS + 语义搜索(推荐)',
|
||||
'codexlens.vectorIndexDesc': '仅语义嵌入搜索',
|
||||
'codexlens.ftsIndexDesc': '仅快速全文搜索',
|
||||
'codexlens.indexTypeHint': '完整索引包含 FTS + 语义搜索。仅 FTS 更快但无 AI 搜索功能。',
|
||||
'codexlens.maintenance': '维护',
|
||||
'codexlens.testSearch': '测试搜索',
|
||||
'codexlens.testFunctionality': '测试 CodexLens 功能',
|
||||
'codexlens.textSearch': '文本搜索',
|
||||
@@ -1673,6 +2027,9 @@ const i18n = {
|
||||
'codexlens.runSearch': '运行搜索',
|
||||
'codexlens.results': '结果',
|
||||
'codexlens.resultsCount': '个结果',
|
||||
'codexlens.resultLimit': '数量限制',
|
||||
'codexlens.contentLength': '内容长度',
|
||||
'codexlens.extraFiles': '额外文件',
|
||||
'codexlens.saveConfig': '保存配置',
|
||||
'codexlens.searching': '搜索中...',
|
||||
'codexlens.searchCompleted': '搜索完成',
|
||||
@@ -1707,6 +2064,18 @@ const i18n = {
|
||||
'codexlens.cudaModeDesc': 'NVIDIA GPU(需要 CUDA Toolkit)',
|
||||
'common.recommended': '推荐',
|
||||
'common.unavailable': '不可用',
|
||||
'common.auto': '自动',
|
||||
|
||||
// GPU 设备选择
|
||||
'codexlens.selectGpuDevice': '选择 GPU 设备',
|
||||
'codexlens.discrete': '独立显卡',
|
||||
'codexlens.integrated': '集成显卡',
|
||||
'codexlens.selectingGpu': '选择 GPU 中...',
|
||||
'codexlens.gpuSelected': 'GPU 已选择',
|
||||
'codexlens.resettingGpu': '重置 GPU 选择中...',
|
||||
'codexlens.gpuReset': 'GPU 选择已重置为自动',
|
||||
'codexlens.resetToAuto': '重置为自动',
|
||||
|
||||
'codexlens.modelManagement': '模型管理',
|
||||
'codexlens.loadingModels': '加载模型中...',
|
||||
'codexlens.downloadModel': '下载',
|
||||
@@ -1758,6 +2127,8 @@ const i18n = {
|
||||
'codexlens.indexComplete': '索引完成',
|
||||
'codexlens.indexSuccess': '索引创建成功',
|
||||
'codexlens.indexFailed': '索引失败',
|
||||
'codexlens.embeddingsFailed': '嵌入生成失败',
|
||||
'codexlens.ftsSuccessEmbeddingsFailed': 'FTS 索引已创建,但嵌入生成失败',
|
||||
|
||||
// CodexLens 安装
|
||||
'codexlens.installDesc': '基于 Python 的代码索引引擎',
|
||||
@@ -1860,6 +2231,19 @@ const i18n = {
|
||||
'lang.windowsDisableSuccess': 'Windows 平台规范已禁用',
|
||||
'lang.windowsEnableFailed': '启用 Windows 平台规范失败',
|
||||
'lang.windowsDisableFailed': '禁用 Windows 平台规范失败',
|
||||
'lang.installRequired': '请运行 "ccw install" 以启用此功能',
|
||||
|
||||
// CCW 安装状态
|
||||
'status.installed': '已安装',
|
||||
'status.incomplete': '不完整',
|
||||
'status.notInstalled': '未安装',
|
||||
'status.ccwInstall': 'CCW 工作流',
|
||||
'status.ccwInstallDesc': '完整功能所需的工作流文件',
|
||||
'status.required': '必需',
|
||||
'status.filesMissing': '个文件缺失',
|
||||
'status.missingFiles': '缺失文件',
|
||||
'status.runToFix': '修复命令',
|
||||
|
||||
'cli.promptFormat': '提示词格式',
|
||||
'cli.promptFormatDesc': '多轮对话拼接格式',
|
||||
'cli.storageBackend': '存储后端',
|
||||
@@ -1872,7 +2256,9 @@ const i18n = {
|
||||
'cli.recursiveQueryDesc': '聚合显示父项目和子项目的 CLI 历史与内存数据',
|
||||
'cli.maxContextFiles': '最大上下文文件数',
|
||||
'cli.maxContextFilesDesc': '智能上下文包含的最大文件数',
|
||||
|
||||
'cli.codeIndexMcp': '代码索引 MCP',
|
||||
'cli.codeIndexMcpDesc': '代码搜索提供者 (更新 CLAUDE.md 的 context-tools 引用)',
|
||||
|
||||
// CCW Install
|
||||
'ccw.install': 'CCW 安装',
|
||||
'ccw.installations': '个安装',
|
||||
@@ -2714,6 +3100,205 @@ const i18n = {
|
||||
'claudeManager.saved': 'File saved successfully',
|
||||
'claudeManager.saveError': 'Failed to save file',
|
||||
|
||||
|
||||
// API Settings
|
||||
'nav.apiSettings': 'API 设置',
|
||||
'title.apiSettings': 'API 设置',
|
||||
'apiSettings.providers': '提供商',
|
||||
'apiSettings.customEndpoints': '自定义端点',
|
||||
'apiSettings.cacheSettings': '缓存设置',
|
||||
'apiSettings.addProvider': '添加提供商',
|
||||
'apiSettings.editProvider': '编辑提供商',
|
||||
'apiSettings.deleteProvider': '删除提供商',
|
||||
'apiSettings.addEndpoint': '添加端点',
|
||||
'apiSettings.editEndpoint': '编辑端点',
|
||||
'apiSettings.deleteEndpoint': '删除端点',
|
||||
'apiSettings.providerType': '提供商类型',
|
||||
'apiSettings.apiFormat': 'API 格式',
|
||||
'apiSettings.compatible': '兼容',
|
||||
'apiSettings.customFormat': '自定义格式',
|
||||
'apiSettings.apiFormatHint': '大多数供应商(DeepSeek、Ollama 等)使用 OpenAI 兼容格式',
|
||||
'apiSettings.displayName': '显示名称',
|
||||
'apiSettings.apiKey': 'API 密钥',
|
||||
'apiSettings.apiBaseUrl': 'API 基础 URL',
|
||||
'apiSettings.useEnvVar': '使用环境变量',
|
||||
'apiSettings.enableProvider': '启用提供商',
|
||||
'apiSettings.advancedSettings': '高级设置',
|
||||
'apiSettings.basicInfo': '基本信息',
|
||||
'apiSettings.endpointSettings': '端点设置',
|
||||
'apiSettings.timeout': '超时时间(秒)',
|
||||
'apiSettings.seconds': '秒',
|
||||
'apiSettings.timeoutHint': '请求超时时间,单位秒(默认:300)',
|
||||
'apiSettings.maxRetries': '最大重试次数',
|
||||
'apiSettings.maxRetriesHint': '失败后最大重试次数',
|
||||
'apiSettings.organization': '组织 ID',
|
||||
'apiSettings.organizationHint': 'OpenAI 组织 ID(org-...)',
|
||||
'apiSettings.apiVersion': 'API 版本',
|
||||
'apiSettings.apiVersionHint': 'Azure API 版本(如 2024-02-01)',
|
||||
'apiSettings.rpm': 'RPM 限制',
|
||||
'apiSettings.tpm': 'TPM 限制',
|
||||
'apiSettings.unlimited': '无限制',
|
||||
'apiSettings.proxy': '代理服务器',
|
||||
'apiSettings.proxyHint': 'HTTP 代理服务器 URL',
|
||||
'apiSettings.customHeaders': '自定义请求头',
|
||||
'apiSettings.customHeadersHint': '自定义 HTTP 请求头的 JSON 对象',
|
||||
'apiSettings.invalidJsonHeaders': '自定义请求头 JSON 格式无效',
|
||||
'apiSettings.searchProviders': '搜索供应商...',
|
||||
'apiSettings.selectProvider': '选择供应商',
|
||||
'apiSettings.selectProviderHint': '从列表中选择一个供应商来查看和管理其设置',
|
||||
'apiSettings.noProvidersFound': '未找到供应商',
|
||||
'apiSettings.llmModels': '大语言模型',
|
||||
'apiSettings.embeddingModels': '向量模型',
|
||||
'apiSettings.manageModels': '管理',
|
||||
'apiSettings.addModel': '添加模型',
|
||||
'apiSettings.multiKeySettings': '多密钥设置',
|
||||
'apiSettings.noModels': '暂无模型配置',
|
||||
'apiSettings.previewModel': '预览',
|
||||
'apiSettings.modelSettings': '模型设置',
|
||||
'apiSettings.deleteModel': '删除模型',
|
||||
'apiSettings.endpointPreview': '端点预览',
|
||||
'apiSettings.modelBaseUrlOverride': '基础 URL 覆盖',
|
||||
'apiSettings.modelBaseUrlHint': '为此模型覆盖供应商的基础 URL(留空则使用供应商默认值)',
|
||||
'apiSettings.providerUpdated': '供应商已更新',
|
||||
'apiSettings.syncToCodexLens': '同步到 CodexLens',
|
||||
'apiSettings.configSynced': '配置已同步到 CodexLens',
|
||||
'apiSettings.preview': '预览',
|
||||
'apiSettings.used': '已使用',
|
||||
'apiSettings.total': '总计',
|
||||
'apiSettings.testConnection': '测试连接',
|
||||
'apiSettings.endpointId': '端点 ID',
|
||||
'apiSettings.endpointIdHint': '用法: ccw cli -p "..." --model <端点ID>',
|
||||
'apiSettings.endpoints': '端点',
|
||||
'apiSettings.addEndpointHint': '创建用于 CLI 的自定义端点别名',
|
||||
'apiSettings.endpointModel': '模型',
|
||||
'apiSettings.selectEndpoint': '选择端点',
|
||||
'apiSettings.selectEndpointHint': '从列表中选择一个端点以查看或编辑其设置',
|
||||
'apiSettings.provider': '提供商',
|
||||
'apiSettings.model': '模型',
|
||||
'apiSettings.selectModel': '选择模型',
|
||||
'apiSettings.noModelsConfigured': '该供应商未配置模型',
|
||||
'apiSettings.cacheStrategy': '缓存策略',
|
||||
'apiSettings.enableContextCaching': '启用上下文缓存',
|
||||
'apiSettings.cacheTTL': 'TTL (分钟)',
|
||||
'apiSettings.cacheMaxSize': '最大大小 (KB)',
|
||||
'apiSettings.autoCachePatterns': '自动缓存文件模式',
|
||||
'apiSettings.enableGlobalCaching': '启用全局缓存',
|
||||
'apiSettings.cacheUsed': '已使用',
|
||||
'apiSettings.cacheEntries': '条目数',
|
||||
'apiSettings.clearCache': '清除缓存',
|
||||
'apiSettings.noProviders': '未配置提供商',
|
||||
'apiSettings.noEndpoints': '未配置端点',
|
||||
'apiSettings.enabled': '已启用',
|
||||
'apiSettings.disabled': '已禁用',
|
||||
'apiSettings.cacheEnabled': '缓存已启用',
|
||||
'apiSettings.cacheDisabled': '缓存已禁用',
|
||||
'apiSettings.providerSaved': '提供商保存成功',
|
||||
'apiSettings.providerDeleted': '提供商删除成功',
|
||||
'apiSettings.apiBaseUpdated': 'API 基础 URL 更新成功',
|
||||
'apiSettings.endpointSaved': '端点保存成功',
|
||||
'apiSettings.endpointDeleted': '端点删除成功',
|
||||
'apiSettings.cacheCleared': '缓存清除成功',
|
||||
'apiSettings.cacheSettingsUpdated': '缓存设置已更新',
|
||||
'apiSettings.embeddingPool': '高可用嵌入',
|
||||
'apiSettings.embeddingPoolDesc': '自动轮训相同模型的供应商',
|
||||
'apiSettings.targetModel': '目标模型',
|
||||
'apiSettings.discoveredProviders': '发现的供应商',
|
||||
'apiSettings.autoDiscover': '自动发现供应商',
|
||||
'apiSettings.excludeProvider': '排除',
|
||||
'apiSettings.defaultCooldown': '冷却时间(秒)',
|
||||
'apiSettings.defaultConcurrent': '每密钥并发数',
|
||||
'apiSettings.poolEnabled': '启用嵌入池',
|
||||
'apiSettings.noProvidersFound': '未找到提供此模型的供应商',
|
||||
'apiSettings.poolSaved': '嵌入池配置已保存',
|
||||
'apiSettings.strategy': '策略',
|
||||
'apiSettings.providerKeys': '密钥',
|
||||
'apiSettings.selectTargetModel': '选择目标模型',
|
||||
'apiSettings.confirmDeleteProvider': '确定要删除此提供商吗?',
|
||||
'apiSettings.confirmDeleteEndpoint': '确定要删除此端点吗?',
|
||||
'apiSettings.confirmClearCache': '确定要清除缓存吗?',
|
||||
'apiSettings.connectionSuccess': '连接成功',
|
||||
'apiSettings.connectionFailed': '连接失败',
|
||||
'apiSettings.saveProviderFirst': '请先保存提供商',
|
||||
'apiSettings.addProviderFirst': '请先添加提供商',
|
||||
'apiSettings.failedToLoad': '加载 API 设置失败',
|
||||
'apiSettings.toggleVisibility': '切换可见性',
|
||||
'apiSettings.noProvidersHint': '添加 API 提供商以开始使用',
|
||||
'apiSettings.noEndpointsHint': '创建自定义端点以快速访问模型',
|
||||
'apiSettings.cache': '缓存',
|
||||
'apiSettings.off': '关闭',
|
||||
'apiSettings.used': '已用',
|
||||
'apiSettings.total': '总计',
|
||||
'apiSettings.cacheUsage': '使用率',
|
||||
'apiSettings.cacheSize': '大小',
|
||||
'apiSettings.endpointsDescription': '管理自定义 API 端点以快速访问模型',
|
||||
'apiSettings.totalEndpoints': '总端点数',
|
||||
'apiSettings.cachedEndpoints': '缓存端点数',
|
||||
'apiSettings.cacheTabHint': '在主面板中配置全局缓存设置并查看统计信息',
|
||||
'apiSettings.cacheDescription': '管理响应缓存以提高性能并降低成本',
|
||||
'apiSettings.cachedEntries': '缓存条目',
|
||||
'apiSettings.storageUsed': '已用存储',
|
||||
'apiSettings.cacheActions': '缓存操作',
|
||||
'apiSettings.cacheStatistics': '缓存统计',
|
||||
'apiSettings.globalCache': '全局缓存',
|
||||
|
||||
// Multi-key management
|
||||
'apiSettings.apiKeys': 'API 密钥',
|
||||
'apiSettings.addKey': '添加密钥',
|
||||
'apiSettings.keyLabel': '标签',
|
||||
'apiSettings.keyValue': 'API 密钥',
|
||||
'apiSettings.keyWeight': '权重',
|
||||
'apiSettings.removeKey': '移除',
|
||||
'apiSettings.noKeys': '未配置 API 密钥',
|
||||
'apiSettings.primaryKey': '主密钥',
|
||||
|
||||
// Routing strategy
|
||||
'apiSettings.routingStrategy': '路由策略',
|
||||
'apiSettings.simpleShuffleRouting': '简单随机',
|
||||
'apiSettings.weightedRouting': '权重分配',
|
||||
'apiSettings.latencyRouting': '延迟优先',
|
||||
'apiSettings.costRouting': '成本优先',
|
||||
'apiSettings.leastBusyRouting': '最少并发',
|
||||
'apiSettings.routingHint': '如何在多个 API 密钥间分配请求',
|
||||
|
||||
// Health check
|
||||
'apiSettings.healthCheck': '健康检查',
|
||||
'apiSettings.enableHealthCheck': '启用健康检查',
|
||||
'apiSettings.healthInterval': '检查间隔(秒)',
|
||||
'apiSettings.healthCooldown': '冷却时间(秒)',
|
||||
'apiSettings.failureThreshold': '失败阈值',
|
||||
'apiSettings.healthStatus': '状态',
|
||||
'apiSettings.healthy': '健康',
|
||||
'apiSettings.unhealthy': '异常',
|
||||
'apiSettings.unknown': '未知',
|
||||
'apiSettings.lastCheck': '最后检查',
|
||||
'apiSettings.testKey': '测试密钥',
|
||||
'apiSettings.testingKey': '测试中...',
|
||||
'apiSettings.keyValid': '密钥有效',
|
||||
'apiSettings.keyInvalid': '密钥无效',
|
||||
|
||||
// Embedding models
|
||||
'apiSettings.embeddingDimensions': '向量维度',
|
||||
'apiSettings.embeddingMaxTokens': '最大 Token',
|
||||
'apiSettings.selectEmbeddingModel': '选择嵌入模型',
|
||||
|
||||
// Model modal
|
||||
'apiSettings.addLlmModel': '添加 LLM 模型',
|
||||
'apiSettings.addEmbeddingModel': '添加嵌入模型',
|
||||
'apiSettings.modelId': '模型 ID',
|
||||
'apiSettings.modelName': '显示名称',
|
||||
'apiSettings.modelSeries': '模型系列',
|
||||
'apiSettings.selectFromPresets': '从预设选择',
|
||||
'apiSettings.customModel': '自定义模型',
|
||||
'apiSettings.capabilities': '能力',
|
||||
'apiSettings.streaming': '流式输出',
|
||||
'apiSettings.functionCalling': '函数调用',
|
||||
'apiSettings.vision': '视觉能力',
|
||||
'apiSettings.contextWindow': '上下文窗口',
|
||||
'apiSettings.description': '描述',
|
||||
'apiSettings.optional': '可选',
|
||||
'apiSettings.modelIdExists': '模型 ID 已存在',
|
||||
'apiSettings.useModelTreeToManage': '使用模型树管理各个模型',
|
||||
|
||||
// Common
|
||||
'common.cancel': '取消',
|
||||
'common.optional': '(可选)',
|
||||
@@ -2737,6 +3322,7 @@ const i18n = {
|
||||
'common.saveFailed': '保存失败',
|
||||
'common.unknownError': '未知错误',
|
||||
'common.exception': '异常',
|
||||
'common.status': '状态',
|
||||
|
||||
// Core Memory
|
||||
'title.coreMemory': '核心记忆',
|
||||
|
||||
3362
ccw/src/templates/dashboard-js/views/api-settings.js
Normal file
3362
ccw/src/templates/dashboard-js/views/api-settings.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,26 @@ var ccwEndpointTools = [];
|
||||
var cliToolConfig = null; // Store loaded CLI config
|
||||
var predefinedModels = {}; // Store predefined models per tool
|
||||
|
||||
// ========== Navigation Helpers ==========
|
||||
|
||||
/**
|
||||
* Navigate to CodexLens Manager page
|
||||
*/
|
||||
function navigateToCodexLensManager() {
|
||||
var navItem = document.querySelector('.nav-item[data-view="codexlens-manager"]');
|
||||
if (navItem) {
|
||||
navItem.click();
|
||||
} else {
|
||||
// Fallback: try to render directly
|
||||
if (typeof renderCodexLensManager === 'function') {
|
||||
currentView = 'codexlens-manager';
|
||||
renderCodexLensManager();
|
||||
} else {
|
||||
showRefreshToast(t('common.error') + ': CodexLens Manager not available', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CCW Installations ==========
|
||||
async function loadCcwInstallations() {
|
||||
try {
|
||||
@@ -39,6 +59,91 @@ async function loadCcwEndpointTools() {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== LiteLLM API Endpoints ==========
|
||||
var litellmApiEndpoints = [];
|
||||
var cliCustomEndpoints = [];
|
||||
|
||||
async function loadLitellmApiEndpoints() {
|
||||
try {
|
||||
var response = await fetch('/api/litellm-api/config');
|
||||
if (!response.ok) throw new Error('Failed to load LiteLLM endpoints');
|
||||
var data = await response.json();
|
||||
litellmApiEndpoints = data.endpoints || [];
|
||||
window.litellmApiConfig = data;
|
||||
return litellmApiEndpoints;
|
||||
} catch (err) {
|
||||
console.error('Failed to load LiteLLM endpoints:', err);
|
||||
litellmApiEndpoints = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCliCustomEndpoints() {
|
||||
try {
|
||||
var response = await fetch('/api/cli/endpoints');
|
||||
if (!response.ok) throw new Error('Failed to load CLI custom endpoints');
|
||||
var data = await response.json();
|
||||
cliCustomEndpoints = data.endpoints || [];
|
||||
return cliCustomEndpoints;
|
||||
} catch (err) {
|
||||
console.error('Failed to load CLI custom endpoints:', err);
|
||||
cliCustomEndpoints = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleEndpointEnabled(endpointId, enabled) {
|
||||
try {
|
||||
var response = await fetch('/api/cli/endpoints/' + endpointId, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: enabled })
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to update endpoint');
|
||||
var data = await response.json();
|
||||
if (data.success) {
|
||||
// Update local state
|
||||
var idx = cliCustomEndpoints.findIndex(function(e) { return e.id === endpointId; });
|
||||
if (idx >= 0) {
|
||||
cliCustomEndpoints[idx].enabled = enabled;
|
||||
}
|
||||
showRefreshToast((enabled ? 'Enabled' : 'Disabled') + ' endpoint: ' + endpointId, 'success');
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
showRefreshToast('Failed to update endpoint: ' + err.message, 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function syncEndpointToCliTools(endpoint) {
|
||||
try {
|
||||
var response = await fetch('/api/cli/endpoints', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id: endpoint.id,
|
||||
name: endpoint.name,
|
||||
enabled: true
|
||||
})
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to sync endpoint');
|
||||
var data = await response.json();
|
||||
if (data.success) {
|
||||
cliCustomEndpoints = data.endpoints;
|
||||
showRefreshToast('Endpoint synced to CLI tools: ' + endpoint.id, 'success');
|
||||
renderToolsSection();
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
showRefreshToast('Failed to sync endpoint: ' + err.message, 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
window.toggleEndpointEnabled = toggleEndpointEnabled;
|
||||
window.syncEndpointToCliTools = syncEndpointToCliTools;
|
||||
|
||||
// ========== CLI Tool Configuration ==========
|
||||
async function loadCliToolConfig() {
|
||||
try {
|
||||
@@ -302,7 +407,9 @@ async function renderCliManager() {
|
||||
loadCliToolStatus(),
|
||||
loadCodexLensStatus(),
|
||||
loadCcwInstallations(),
|
||||
loadCcwEndpointTools()
|
||||
loadCcwEndpointTools(),
|
||||
loadLitellmApiEndpoints(),
|
||||
loadCliCustomEndpoints()
|
||||
]);
|
||||
|
||||
container.innerHTML = '<div class="status-manager">' +
|
||||
@@ -314,8 +421,7 @@ async function renderCliManager() {
|
||||
'<div class="cli-settings-section" id="cli-settings-section" style="margin-top: 1.5rem;"></div>' +
|
||||
'<div class="cli-section" id="ccw-endpoint-tools-section" style="margin-top: 1.5rem;"></div>' +
|
||||
'</div>' +
|
||||
'<section id="storageCard" class="mb-6"></section>' +
|
||||
'<section id="indexCard" class="mb-6"></section>';
|
||||
'<section id="storageCard" class="mb-6"></section>';
|
||||
|
||||
// Render sub-panels
|
||||
renderToolsSection();
|
||||
@@ -329,11 +435,6 @@ async function renderCliManager() {
|
||||
initStorageManager();
|
||||
}
|
||||
|
||||
// Initialize index manager card
|
||||
if (typeof initIndexManager === 'function') {
|
||||
initIndexManager();
|
||||
}
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
@@ -434,28 +535,22 @@ function renderToolsSection() {
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
// CodexLens item
|
||||
var codexLensHtml = '<div class="tool-item clickable ' + (codexLensStatus.ready ? 'available' : 'unavailable') + '" onclick="showCodexLensConfigModal()">' +
|
||||
// CodexLens item - simplified view with link to manager page
|
||||
var codexLensHtml = '<div class="tool-item clickable ' + (codexLensStatus.ready ? 'available' : 'unavailable') + '" onclick="navigateToCodexLensManager()">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot ' + (codexLensStatus.ready ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">CodexLens <span class="tool-type-badge">Index</span>' +
|
||||
'<i data-lucide="settings" class="w-3 h-3 tool-config-icon"></i></div>' +
|
||||
'<i data-lucide="external-link" class="w-3 h-3 tool-config-icon"></i></div>' +
|
||||
'<div class="tool-item-desc">' + (codexLensStatus.ready ? t('cli.codexLensDesc') : t('cli.codexLensDescFull')) + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
(codexLensStatus.ready
|
||||
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> v' + (codexLensStatus.version || 'installed') + '</span>' +
|
||||
'<select id="codexlensModelSelect" class="btn-sm bg-muted border border-border rounded text-xs" onclick="event.stopPropagation()" title="' + (t('index.selectModel') || 'Select embedding model') + '">' +
|
||||
buildModelSelectOptions() +
|
||||
'</select>' +
|
||||
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); initCodexLensIndex(\'full\', getSelectedModel())" title="' + (t('index.fullDesc') || 'FTS + Semantic search (recommended)') + '"><i data-lucide="layers" class="w-3 h-3"></i> ' + (t('index.fullIndex') || '全部索引') + '</button>' +
|
||||
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex(\'vector\', getSelectedModel())" title="' + (t('index.vectorDesc') || 'Semantic search with embeddings') + '"><i data-lucide="sparkles" class="w-3 h-3"></i> ' + (t('index.vectorIndex') || '向量索引') + '</button>' +
|
||||
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex(\'normal\')" title="' + (t('index.normalDesc') || 'Fast full-text search only') + '"><i data-lucide="file-text" class="w-3 h-3"></i> ' + (t('index.normalIndex') || 'FTS索引') + '</button>' +
|
||||
'<button class="btn-sm btn-outline btn-danger" onclick="event.stopPropagation(); uninstallCodexLens()"><i data-lucide="trash-2" class="w-3 h-3"></i> ' + t('cli.uninstall') + '</button>'
|
||||
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); navigateToCodexLensManager()"><i data-lucide="settings" class="w-3 h-3"></i> ' + t('cli.openManager') + '</button>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> ' + t('cli.notInstalled') + '</span>' +
|
||||
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); installCodexLens()"><i data-lucide="download" class="w-3 h-3"></i> ' + t('cli.install') + '</button>') +
|
||||
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); navigateToCodexLensManager()"><i data-lucide="settings" class="w-3 h-3"></i> ' + t('cli.openManager') + '</button>') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
@@ -479,6 +574,51 @@ function renderToolsSection() {
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// API Endpoints section
|
||||
var apiEndpointsHtml = '';
|
||||
if (litellmApiEndpoints.length > 0) {
|
||||
var endpointItems = litellmApiEndpoints.map(function(endpoint) {
|
||||
// Check if endpoint is synced to CLI tools
|
||||
var cliEndpoint = cliCustomEndpoints.find(function(e) { return e.id === endpoint.id; });
|
||||
var isSynced = !!cliEndpoint;
|
||||
var isEnabled = cliEndpoint ? cliEndpoint.enabled : false;
|
||||
|
||||
// Find provider info
|
||||
var provider = (window.litellmApiConfig?.providers || []).find(function(p) { return p.id === endpoint.providerId; });
|
||||
var providerName = provider ? provider.name : endpoint.providerId;
|
||||
|
||||
return '<div class="tool-item ' + (isSynced && isEnabled ? 'available' : 'unavailable') + '">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot ' + (isSynced && isEnabled ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">' + endpoint.id + ' <span class="tool-type-badge">API</span></div>' +
|
||||
'<div class="tool-item-desc">' + endpoint.model + ' (' + providerName + ')</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
(isSynced
|
||||
? '<label class="toggle-switch" onclick="event.stopPropagation()">' +
|
||||
'<input type="checkbox" ' + (isEnabled ? 'checked' : '') + ' onchange="toggleEndpointEnabled(\'' + endpoint.id + '\', this.checked); renderToolsSection();">' +
|
||||
'<span class="toggle-slider"></span>' +
|
||||
'</label>'
|
||||
: '<button class="btn-sm btn-primary" onclick="event.stopPropagation(); syncEndpointToCliTools({id: \'' + endpoint.id + '\', name: \'' + endpoint.name + '\'})">' +
|
||||
'<i data-lucide="plus" class="w-3 h-3"></i> ' + (t('cli.addToCli') || 'Add to CLI') +
|
||||
'</button>') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
apiEndpointsHtml = '<div class="tools-subsection" style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border);">' +
|
||||
'<div class="section-header-left" style="margin-bottom: 0.5rem;">' +
|
||||
'<h4 style="font-size: 0.875rem; font-weight: 600; display: flex; align-items: center; gap: 0.5rem;">' +
|
||||
'<i data-lucide="cloud" class="w-4 h-4"></i> ' + (t('cli.apiEndpoints') || 'API Endpoints') +
|
||||
'</h4>' +
|
||||
'<span class="section-count">' + litellmApiEndpoints.length + ' ' + (t('cli.configured') || 'configured') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="tools-list">' + endpointItems + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = '<div class="section-header">' +
|
||||
'<div class="section-header-left">' +
|
||||
'<h3><i data-lucide="terminal" class="w-4 h-4"></i> ' + t('cli.tools') + '</h3>' +
|
||||
@@ -492,7 +632,8 @@ function renderToolsSection() {
|
||||
toolsHtml +
|
||||
codexLensHtml +
|
||||
semanticHtml +
|
||||
'</div>';
|
||||
'</div>' +
|
||||
apiEndpointsHtml;
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
@@ -606,6 +747,16 @@ async function loadWindowsPlatformSettings() {
|
||||
|
||||
async function toggleChineseResponse(enabled) {
|
||||
if (chineseResponseLoading) return;
|
||||
|
||||
// Pre-check: verify CCW workflows are installed (only when enabling)
|
||||
if (enabled && typeof ccwInstallStatus !== 'undefined' && !ccwInstallStatus.installed) {
|
||||
var missingFile = ccwInstallStatus.missingFiles.find(function(f) { return f === 'chinese-response.md'; });
|
||||
if (missingFile) {
|
||||
showRefreshToast(t('lang.installRequired'), 'warning');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
chineseResponseLoading = true;
|
||||
|
||||
try {
|
||||
@@ -617,7 +768,14 @@ async function toggleChineseResponse(enabled) {
|
||||
|
||||
if (!response.ok) {
|
||||
var errData = await response.json();
|
||||
throw new Error(errData.error || 'Failed to update setting');
|
||||
// Show specific error message from backend
|
||||
var errorMsg = errData.error || 'Failed to update setting';
|
||||
if (errorMsg.includes('not found')) {
|
||||
showRefreshToast(t('lang.installRequired'), 'warning');
|
||||
} else {
|
||||
showRefreshToast((enabled ? t('lang.enableFailed') : t('lang.disableFailed')) + ': ' + errorMsg, 'error');
|
||||
}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
var data = await response.json();
|
||||
@@ -630,7 +788,7 @@ async function toggleChineseResponse(enabled) {
|
||||
showRefreshToast(enabled ? t('lang.enableSuccess') : t('lang.disableSuccess'), 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to toggle Chinese response:', err);
|
||||
showRefreshToast(enabled ? t('lang.enableFailed') : t('lang.disableFailed'), 'error');
|
||||
// Error already shown in the !response.ok block
|
||||
} finally {
|
||||
chineseResponseLoading = false;
|
||||
}
|
||||
@@ -638,6 +796,16 @@ async function toggleChineseResponse(enabled) {
|
||||
|
||||
async function toggleWindowsPlatform(enabled) {
|
||||
if (windowsPlatformLoading) return;
|
||||
|
||||
// Pre-check: verify CCW workflows are installed (only when enabling)
|
||||
if (enabled && typeof ccwInstallStatus !== 'undefined' && !ccwInstallStatus.installed) {
|
||||
var missingFile = ccwInstallStatus.missingFiles.find(function(f) { return f === 'windows-platform.md'; });
|
||||
if (missingFile) {
|
||||
showRefreshToast(t('lang.installRequired'), 'warning');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
windowsPlatformLoading = true;
|
||||
|
||||
try {
|
||||
@@ -649,7 +817,14 @@ async function toggleWindowsPlatform(enabled) {
|
||||
|
||||
if (!response.ok) {
|
||||
var errData = await response.json();
|
||||
throw new Error(errData.error || 'Failed to update setting');
|
||||
// Show specific error message from backend
|
||||
var errorMsg = errData.error || 'Failed to update setting';
|
||||
if (errorMsg.includes('not found')) {
|
||||
showRefreshToast(t('lang.installRequired'), 'warning');
|
||||
} else {
|
||||
showRefreshToast((enabled ? t('lang.windowsEnableFailed') : t('lang.windowsDisableFailed')) + ': ' + errorMsg, 'error');
|
||||
}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
var data = await response.json();
|
||||
@@ -662,7 +837,7 @@ async function toggleWindowsPlatform(enabled) {
|
||||
showRefreshToast(enabled ? t('lang.windowsEnableSuccess') : t('lang.windowsDisableSuccess'), 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to toggle Windows platform:', err);
|
||||
showRefreshToast(enabled ? t('lang.windowsEnableFailed') : t('lang.windowsDisableFailed'), 'error');
|
||||
// Error already shown in the !response.ok block
|
||||
} finally {
|
||||
windowsPlatformLoading = false;
|
||||
}
|
||||
@@ -812,6 +987,24 @@ function renderCliSettingsSection() {
|
||||
'</div>' +
|
||||
'<p class="cli-setting-desc">' + t('cli.maxContextFilesDesc') + '</p>' +
|
||||
'</div>' +
|
||||
'<div class="cli-setting-item">' +
|
||||
'<label class="cli-setting-label">' +
|
||||
'<i data-lucide="search" class="w-3 h-3"></i>' +
|
||||
t('cli.codeIndexMcp') +
|
||||
'</label>' +
|
||||
'<div class="cli-setting-control">' +
|
||||
'<select class="cli-setting-select" onchange="setCodeIndexMcpProvider(this.value)">' +
|
||||
'<option value="codexlens"' + (codeIndexMcpProvider === 'codexlens' ? ' selected' : '') + '>CodexLens</option>' +
|
||||
'<option value="ace"' + (codeIndexMcpProvider === 'ace' ? ' selected' : '') + '>ACE (Augment)</option>' +
|
||||
'<option value="none"' + (codeIndexMcpProvider === 'none' ? ' selected' : '') + '>None (Built-in)</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<p class="cli-setting-desc">' + t('cli.codeIndexMcpDesc') + '</p>' +
|
||||
'<p class="cli-setting-desc text-xs text-muted-foreground">' +
|
||||
'<i data-lucide="file-text" class="w-3 h-3 inline-block mr-1"></i>' +
|
||||
'Current: <code class="bg-muted px-1 rounded">' + getContextToolsFileName(codeIndexMcpProvider) + '</code>' +
|
||||
'</p>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
container.innerHTML = settingsHtml;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -69,8 +69,10 @@ async function renderCliHistoryView() {
|
||||
'</div>'
|
||||
: '';
|
||||
|
||||
// Normalize sourceDir: convert backslashes to forward slashes for safe onclick handling
|
||||
var normalizedSourceDir = (exec.sourceDir || '').replace(/\\/g, '/');
|
||||
historyHtml += '<div class="history-item' + (isSelected ? ' history-item-selected' : '') + '" ' +
|
||||
'onclick="' + (isMultiSelectMode ? 'toggleExecutionSelection(\'' + exec.id + '\')' : 'showExecutionDetail(\'' + exec.id + (exec.sourceDir ? '\',\'' + escapeHtml(exec.sourceDir) : '') + '\')') + '">' +
|
||||
'onclick="' + (isMultiSelectMode ? 'toggleExecutionSelection(\'' + exec.id + '\')' : 'showExecutionDetail(\'' + exec.id + '\', \'' + normalizedSourceDir.replace(/'/g, "\\'") + '\')') + '">' +
|
||||
checkboxHtml +
|
||||
'<div class="history-item-main">' +
|
||||
'<div class="history-item-header">' +
|
||||
@@ -87,14 +89,17 @@ async function renderCliHistoryView() {
|
||||
'<div class="history-item-meta">' +
|
||||
'<span class="history-time"><i data-lucide="clock" class="w-3 h-3"></i> ' + timeAgo + '</span>' +
|
||||
'<span class="history-duration"><i data-lucide="timer" class="w-3 h-3"></i> ' + duration + '</span>' +
|
||||
'<span class="history-id"><i data-lucide="hash" class="w-3 h-3"></i> ' + exec.id.split('-')[0] + '</span>' +
|
||||
'<span class="history-id" title="' + exec.id + '"><i data-lucide="hash" class="w-3 h-3"></i> ' + exec.id.substring(0, 13) + '...' + exec.id.split('-').pop() + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="history-item-actions">' +
|
||||
'<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail(\'' + exec.id + '\')" title="View Details">' +
|
||||
'<button class="btn-icon" onclick="event.stopPropagation(); copyExecutionId(\'' + exec.id + '\')" title="Copy ID">' +
|
||||
'<i data-lucide="copy" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail(\'' + exec.id + '\', \'' + normalizedSourceDir.replace(/'/g, "\\'") + '\')" title="View Details">' +
|
||||
'<i data-lucide="eye" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution(\'' + exec.id + (exec.sourceDir ? '\',\'' + escapeHtml(exec.sourceDir) : '') + '\')" title="Delete">' +
|
||||
'<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution(\'' + exec.id + '\', \'' + normalizedSourceDir.replace(/'/g, "\\'") + '\')" title="Delete">' +
|
||||
'<i data-lucide="trash-2" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
@@ -179,6 +184,16 @@ async function renderCliHistoryView() {
|
||||
}
|
||||
|
||||
// ========== Actions ==========
|
||||
async function copyExecutionId(executionId) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(executionId);
|
||||
showRefreshToast('ID copied: ' + executionId, 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy ID:', err);
|
||||
showRefreshToast('Failed to copy ID', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function filterCliHistoryView(tool) {
|
||||
cliHistoryFilter = tool || null;
|
||||
await loadCliHistory();
|
||||
|
||||
@@ -331,6 +331,15 @@
|
||||
<i data-lucide="history" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1" data-i18n="nav.history">History</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="codexlens-manager" data-tooltip="CodexLens Manager">
|
||||
<i data-lucide="search-code" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1" data-i18n="nav.codexLensManager">CodexLens</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeCodexLens">-</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="api-settings" data-tooltip="API Settings">
|
||||
<i data-lucide="settings" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1" data-i18n="nav.apiSettings">API Settings</span>
|
||||
</li>
|
||||
<!-- Hidden: Code Graph Explorer (feature disabled)
|
||||
<li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="graph-explorer" data-tooltip="Code Graph Explorer">
|
||||
<i data-lucide="git-branch" class="nav-icon"></i>
|
||||
|
||||
388
ccw/src/tools/claude-cli-tools.ts
Normal file
388
ccw/src/tools/claude-cli-tools.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* Claude CLI Tools Configuration Manager
|
||||
* Manages .claude/cli-tools.json with fallback:
|
||||
* 1. Project workspace: {projectDir}/.claude/cli-tools.json (priority)
|
||||
* 2. Global: ~/.claude/cli-tools.json (fallback)
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
export interface ClaudeCliTool {
|
||||
enabled: boolean;
|
||||
isBuiltin: boolean;
|
||||
command: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ClaudeCacheSettings {
|
||||
injectionMode: 'auto' | 'manual' | 'disabled';
|
||||
defaultPrefix: string;
|
||||
defaultSuffix: string;
|
||||
}
|
||||
|
||||
export interface ClaudeCliToolsConfig {
|
||||
$schema?: string;
|
||||
version: string;
|
||||
tools: Record<string, ClaudeCliTool>;
|
||||
customEndpoints: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
}>;
|
||||
defaultTool: string;
|
||||
settings: {
|
||||
promptFormat: 'plain' | 'yaml' | 'json';
|
||||
smartContext: {
|
||||
enabled: boolean;
|
||||
maxFiles: number;
|
||||
};
|
||||
nativeResume: boolean;
|
||||
recursiveQuery: boolean;
|
||||
cache: ClaudeCacheSettings;
|
||||
codeIndexMcp: 'codexlens' | 'ace' | 'none'; // Code Index MCP provider
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Default Config ==========
|
||||
|
||||
const DEFAULT_CONFIG: ClaudeCliToolsConfig = {
|
||||
version: '1.0.0',
|
||||
tools: {
|
||||
gemini: {
|
||||
enabled: true,
|
||||
isBuiltin: true,
|
||||
command: 'gemini',
|
||||
description: 'Google AI for code analysis'
|
||||
},
|
||||
qwen: {
|
||||
enabled: true,
|
||||
isBuiltin: true,
|
||||
command: 'qwen',
|
||||
description: 'Alibaba AI assistant'
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
isBuiltin: true,
|
||||
command: 'codex',
|
||||
description: 'OpenAI code generation'
|
||||
},
|
||||
claude: {
|
||||
enabled: true,
|
||||
isBuiltin: true,
|
||||
command: 'claude',
|
||||
description: 'Anthropic AI assistant'
|
||||
}
|
||||
},
|
||||
customEndpoints: [],
|
||||
defaultTool: 'gemini',
|
||||
settings: {
|
||||
promptFormat: 'plain',
|
||||
smartContext: {
|
||||
enabled: false,
|
||||
maxFiles: 10
|
||||
},
|
||||
nativeResume: true,
|
||||
recursiveQuery: true,
|
||||
cache: {
|
||||
injectionMode: 'auto',
|
||||
defaultPrefix: '',
|
||||
defaultSuffix: ''
|
||||
},
|
||||
codeIndexMcp: 'codexlens' // Default to CodexLens
|
||||
}
|
||||
};
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
function getProjectConfigPath(projectDir: string): string {
|
||||
return path.join(projectDir, '.claude', 'cli-tools.json');
|
||||
}
|
||||
|
||||
function getGlobalConfigPath(): string {
|
||||
return path.join(os.homedir(), '.claude', 'cli-tools.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve config path with fallback:
|
||||
* 1. Project: {projectDir}/.claude/cli-tools.json
|
||||
* 2. Global: ~/.claude/cli-tools.json
|
||||
* Returns { path, source } where source is 'project' | 'global' | 'default'
|
||||
*/
|
||||
function resolveConfigPath(projectDir: string): { path: string; source: 'project' | 'global' | 'default' } {
|
||||
const projectPath = getProjectConfigPath(projectDir);
|
||||
if (fs.existsSync(projectPath)) {
|
||||
return { path: projectPath, source: 'project' };
|
||||
}
|
||||
|
||||
const globalPath = getGlobalConfigPath();
|
||||
if (fs.existsSync(globalPath)) {
|
||||
return { path: globalPath, source: 'global' };
|
||||
}
|
||||
|
||||
return { path: projectPath, source: 'default' };
|
||||
}
|
||||
|
||||
function ensureClaudeDir(projectDir: string): void {
|
||||
const claudeDir = path.join(projectDir, '.claude');
|
||||
if (!fs.existsSync(claudeDir)) {
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Main Functions ==========
|
||||
|
||||
/**
|
||||
* Load CLI tools configuration with fallback:
|
||||
* 1. Project: {projectDir}/.claude/cli-tools.json
|
||||
* 2. Global: ~/.claude/cli-tools.json
|
||||
* 3. Default config
|
||||
*/
|
||||
export function loadClaudeCliTools(projectDir: string): ClaudeCliToolsConfig & { _source?: string } {
|
||||
const resolved = resolveConfigPath(projectDir);
|
||||
|
||||
try {
|
||||
if (resolved.source === 'default') {
|
||||
// No config file found, return defaults
|
||||
return { ...DEFAULT_CONFIG, _source: 'default' };
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(resolved.path, 'utf-8');
|
||||
const parsed = JSON.parse(content) as Partial<ClaudeCliToolsConfig>;
|
||||
|
||||
// Merge with defaults
|
||||
const config = {
|
||||
...DEFAULT_CONFIG,
|
||||
...parsed,
|
||||
tools: { ...DEFAULT_CONFIG.tools, ...(parsed.tools || {}) },
|
||||
settings: {
|
||||
...DEFAULT_CONFIG.settings,
|
||||
...(parsed.settings || {}),
|
||||
smartContext: {
|
||||
...DEFAULT_CONFIG.settings.smartContext,
|
||||
...(parsed.settings?.smartContext || {})
|
||||
},
|
||||
cache: {
|
||||
...DEFAULT_CONFIG.settings.cache,
|
||||
...(parsed.settings?.cache || {})
|
||||
}
|
||||
},
|
||||
_source: resolved.source
|
||||
};
|
||||
|
||||
console.log(`[claude-cli-tools] Loaded config from ${resolved.source}: ${resolved.path}`);
|
||||
return config;
|
||||
} catch (err) {
|
||||
console.error('[claude-cli-tools] Error loading config:', err);
|
||||
return { ...DEFAULT_CONFIG, _source: 'default' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save CLI tools configuration to project .claude/cli-tools.json
|
||||
* Always saves to project directory (not global)
|
||||
*/
|
||||
export function saveClaudeCliTools(projectDir: string, config: ClaudeCliToolsConfig & { _source?: string }): void {
|
||||
ensureClaudeDir(projectDir);
|
||||
const configPath = getProjectConfigPath(projectDir);
|
||||
|
||||
// Remove internal _source field before saving
|
||||
const { _source, ...configToSave } = config;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8');
|
||||
console.log(`[claude-cli-tools] Saved config to project: ${configPath}`);
|
||||
} catch (err) {
|
||||
console.error('[claude-cli-tools] Error saving config:', err);
|
||||
throw new Error(`Failed to save CLI tools config: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update enabled status for a specific tool
|
||||
*/
|
||||
export function updateClaudeToolEnabled(
|
||||
projectDir: string,
|
||||
toolName: string,
|
||||
enabled: boolean
|
||||
): ClaudeCliToolsConfig {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
|
||||
if (config.tools[toolName]) {
|
||||
config.tools[toolName].enabled = enabled;
|
||||
saveClaudeCliTools(projectDir, config);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cache settings
|
||||
*/
|
||||
export function updateClaudeCacheSettings(
|
||||
projectDir: string,
|
||||
cacheSettings: Partial<ClaudeCacheSettings>
|
||||
): ClaudeCliToolsConfig {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
|
||||
config.settings.cache = {
|
||||
...config.settings.cache,
|
||||
...cacheSettings
|
||||
};
|
||||
|
||||
saveClaudeCliTools(projectDir, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update default tool
|
||||
*/
|
||||
export function updateClaudeDefaultTool(
|
||||
projectDir: string,
|
||||
defaultTool: string
|
||||
): ClaudeCliToolsConfig {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
config.defaultTool = defaultTool;
|
||||
saveClaudeCliTools(projectDir, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom endpoint
|
||||
*/
|
||||
export function addClaudeCustomEndpoint(
|
||||
projectDir: string,
|
||||
endpoint: { id: string; name: string; enabled: boolean }
|
||||
): ClaudeCliToolsConfig {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
|
||||
// Check if endpoint already exists
|
||||
const existingIndex = config.customEndpoints.findIndex(e => e.id === endpoint.id);
|
||||
if (existingIndex >= 0) {
|
||||
config.customEndpoints[existingIndex] = endpoint;
|
||||
} else {
|
||||
config.customEndpoints.push(endpoint);
|
||||
}
|
||||
|
||||
saveClaudeCliTools(projectDir, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove custom endpoint
|
||||
*/
|
||||
export function removeClaudeCustomEndpoint(
|
||||
projectDir: string,
|
||||
endpointId: string
|
||||
): ClaudeCliToolsConfig {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
config.customEndpoints = config.customEndpoints.filter(e => e.id !== endpointId);
|
||||
saveClaudeCliTools(projectDir, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get config source info
|
||||
*/
|
||||
export function getClaudeCliToolsInfo(projectDir: string): {
|
||||
projectPath: string;
|
||||
globalPath: string;
|
||||
activePath: string;
|
||||
source: 'project' | 'global' | 'default';
|
||||
} {
|
||||
const resolved = resolveConfigPath(projectDir);
|
||||
return {
|
||||
projectPath: getProjectConfigPath(projectDir),
|
||||
globalPath: getGlobalConfigPath(),
|
||||
activePath: resolved.path,
|
||||
source: resolved.source
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Code Index MCP provider and switch CLAUDE.md reference
|
||||
* Strategy: Only modify global user-level CLAUDE.md (~/.claude/CLAUDE.md)
|
||||
* This is consistent with Chinese response and Windows platform settings
|
||||
*/
|
||||
export function updateCodeIndexMcp(
|
||||
projectDir: string,
|
||||
provider: 'codexlens' | 'ace' | 'none'
|
||||
): { success: boolean; error?: string; config?: ClaudeCliToolsConfig } {
|
||||
try {
|
||||
// Update config
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
config.settings.codeIndexMcp = provider;
|
||||
saveClaudeCliTools(projectDir, config);
|
||||
|
||||
// Only update global CLAUDE.md (consistent with Chinese response / Windows platform)
|
||||
const globalClaudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
|
||||
|
||||
// Define patterns for all formats
|
||||
const codexlensPattern = /@~\/\.claude\/workflows\/context-tools\.md/g;
|
||||
const acePattern = /@~\/\.claude\/workflows\/context-tools-ace\.md/g;
|
||||
const nonePattern = /@~\/\.claude\/workflows\/context-tools-none\.md/g;
|
||||
|
||||
// Determine target file based on provider
|
||||
const targetFile = provider === 'ace'
|
||||
? '@~/.claude/workflows/context-tools-ace.md'
|
||||
: provider === 'none'
|
||||
? '@~/.claude/workflows/context-tools-none.md'
|
||||
: '@~/.claude/workflows/context-tools.md';
|
||||
|
||||
if (!fs.existsSync(globalClaudeMdPath)) {
|
||||
// If global CLAUDE.md doesn't exist, check project-level
|
||||
const projectClaudeMdPath = path.join(projectDir, '.claude', 'CLAUDE.md');
|
||||
if (fs.existsSync(projectClaudeMdPath)) {
|
||||
let content = fs.readFileSync(projectClaudeMdPath, 'utf-8');
|
||||
|
||||
// Replace any existing pattern with the target
|
||||
content = content.replace(codexlensPattern, targetFile);
|
||||
content = content.replace(acePattern, targetFile);
|
||||
content = content.replace(nonePattern, targetFile);
|
||||
|
||||
fs.writeFileSync(projectClaudeMdPath, content, 'utf-8');
|
||||
console.log(`[claude-cli-tools] Updated project CLAUDE.md to use ${provider} (no global CLAUDE.md found)`);
|
||||
}
|
||||
} else {
|
||||
// Update global CLAUDE.md (primary target)
|
||||
let content = fs.readFileSync(globalClaudeMdPath, 'utf-8');
|
||||
|
||||
// Replace any existing pattern with the target
|
||||
content = content.replace(codexlensPattern, targetFile);
|
||||
content = content.replace(acePattern, targetFile);
|
||||
content = content.replace(nonePattern, targetFile);
|
||||
|
||||
fs.writeFileSync(globalClaudeMdPath, content, 'utf-8');
|
||||
console.log(`[claude-cli-tools] Updated global CLAUDE.md to use ${provider}`);
|
||||
}
|
||||
|
||||
return { success: true, config };
|
||||
} catch (err) {
|
||||
console.error('[claude-cli-tools] Error updating Code Index MCP:', err);
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current Code Index MCP provider
|
||||
*/
|
||||
export function getCodeIndexMcp(projectDir: string): 'codexlens' | 'ace' | 'none' {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
return config.settings.codeIndexMcp || 'codexlens';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context-tools file path based on provider
|
||||
*/
|
||||
export function getContextToolsPath(provider: 'codexlens' | 'ace' | 'none'): string {
|
||||
switch (provider) {
|
||||
case 'ace':
|
||||
return 'context-tools-ace.md';
|
||||
case 'none':
|
||||
return 'context-tools-none.md';
|
||||
default:
|
||||
return 'context-tools.md';
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@ import { spawn, ChildProcess } from 'child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync, statSync } from 'fs';
|
||||
import { join, relative } from 'path';
|
||||
|
||||
// LiteLLM integration
|
||||
import { executeLiteLLMEndpoint } from './litellm-executor.js';
|
||||
import { findEndpointById } from '../config/litellm-api-config-manager.js';
|
||||
|
||||
// Native resume support
|
||||
import {
|
||||
trackNewSession,
|
||||
@@ -63,12 +67,13 @@ const ParamsSchema = z.object({
|
||||
model: z.string().optional(),
|
||||
cd: z.string().optional(),
|
||||
includeDirs: z.string().optional(),
|
||||
timeout: z.number().default(300000),
|
||||
timeout: z.number().default(0), // 0 = no internal timeout, controlled by external caller (e.g., bash timeout)
|
||||
resume: z.union([z.boolean(), z.string()]).optional(), // true = last, string = single ID or comma-separated IDs
|
||||
id: z.string().optional(), // Custom execution ID (e.g., IMPL-001-step1)
|
||||
noNative: z.boolean().optional(), // Force prompt concatenation instead of native resume
|
||||
category: z.enum(['user', 'internal', 'insight']).default('user'), // Execution category for tracking
|
||||
parentExecutionId: z.string().optional(), // Parent execution ID for fork/retry scenarios
|
||||
stream: z.boolean().default(false), // false = cache full output (default), true = stream output via callback
|
||||
});
|
||||
|
||||
// Execution category types
|
||||
@@ -333,9 +338,8 @@ function buildCommand(params: {
|
||||
args.push(nativeResume.sessionId);
|
||||
}
|
||||
// Codex resume still supports additional flags
|
||||
if (dir) {
|
||||
args.push('-C', dir);
|
||||
}
|
||||
// Note: -C is NOT used because spawn's cwd already sets the working directory
|
||||
// Using both would cause path to be applied twice (e.g., codex-lens/codex-lens)
|
||||
// Permission configuration based on mode:
|
||||
// - analysis: --full-auto (read-only sandbox, no prompts) - safer for read operations
|
||||
// - write/auto: --dangerously-bypass-approvals-and-sandbox (full access for modifications)
|
||||
@@ -358,9 +362,8 @@ function buildCommand(params: {
|
||||
} else {
|
||||
// Standard exec mode
|
||||
args.push('exec');
|
||||
if (dir) {
|
||||
args.push('-C', dir);
|
||||
}
|
||||
// Note: -C is NOT used because spawn's cwd already sets the working directory
|
||||
// Using both would cause path to be applied twice (e.g., codex-lens/codex-lens)
|
||||
// Permission configuration based on mode:
|
||||
// - analysis: --full-auto (read-only sandbox, no prompts) - safer for read operations
|
||||
// - write/auto: --dangerously-bypass-approvals-and-sandbox (full access for modifications)
|
||||
@@ -592,6 +595,66 @@ async function executeCliTool(
|
||||
const workingDir = cd || process.cwd();
|
||||
ensureHistoryDir(workingDir); // Ensure history directory exists
|
||||
|
||||
// NEW: Check if model is a custom LiteLLM endpoint ID
|
||||
if (model && !['gemini', 'qwen', 'codex'].includes(tool)) {
|
||||
const endpoint = findEndpointById(workingDir, model);
|
||||
if (endpoint) {
|
||||
// Route to LiteLLM executor
|
||||
if (onOutput) {
|
||||
onOutput({ type: 'stderr', data: `[Routing to LiteLLM endpoint: ${model}]\n` });
|
||||
}
|
||||
|
||||
const result = await executeLiteLLMEndpoint({
|
||||
prompt,
|
||||
endpointId: model,
|
||||
baseDir: workingDir,
|
||||
cwd: cd,
|
||||
includeDirs: includeDirs ? includeDirs.split(',').map(d => d.trim()) : undefined,
|
||||
enableCache: true,
|
||||
onOutput: onOutput || undefined,
|
||||
});
|
||||
|
||||
// Convert LiteLLM result to ExecutionOutput format
|
||||
const startTime = Date.now();
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
const execution: ExecutionRecord = {
|
||||
id: customId || `${Date.now()}-litellm`,
|
||||
timestamp: new Date(startTime).toISOString(),
|
||||
tool: 'litellm',
|
||||
model: result.model,
|
||||
mode,
|
||||
prompt,
|
||||
status: result.success ? 'success' : 'error',
|
||||
exit_code: result.success ? 0 : 1,
|
||||
duration_ms: duration,
|
||||
output: {
|
||||
stdout: result.output,
|
||||
stderr: result.error || '',
|
||||
truncated: false,
|
||||
},
|
||||
};
|
||||
|
||||
const conversation = convertToConversation(execution);
|
||||
|
||||
// Try to save to history
|
||||
try {
|
||||
saveConversation(workingDir, conversation);
|
||||
} catch (err) {
|
||||
console.error('[CLI Executor] Failed to save LiteLLM history:', (err as Error).message);
|
||||
}
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
execution,
|
||||
conversation,
|
||||
stdout: result.output,
|
||||
stderr: result.error || '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get SQLite store for native session lookup
|
||||
const store = await getSqliteStore(workingDir);
|
||||
|
||||
@@ -801,24 +864,36 @@ async function executeCliTool(
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// Determine status
|
||||
// Determine status - prioritize output content over exit code
|
||||
let status: 'success' | 'error' | 'timeout' = 'success';
|
||||
if (timedOut) {
|
||||
status = 'timeout';
|
||||
} else if (code !== 0) {
|
||||
// Check if HTTP 429 but results exist (Gemini quirk)
|
||||
if (stderr.includes('429') && stdout.trim()) {
|
||||
// Non-zero exit code doesn't always mean failure
|
||||
// Check if there's valid output (AI response) - treat as success
|
||||
const hasValidOutput = stdout.trim().length > 0;
|
||||
const hasFatalError = stderr.includes('FATAL') ||
|
||||
stderr.includes('Authentication failed') ||
|
||||
stderr.includes('API key') ||
|
||||
stderr.includes('rate limit exceeded');
|
||||
|
||||
if (hasValidOutput && !hasFatalError) {
|
||||
// Has output and no fatal errors - treat as success despite exit code
|
||||
status = 'success';
|
||||
} else {
|
||||
status = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
// Create new turn
|
||||
// Create new turn - cache full output when not streaming (default)
|
||||
const shouldCache = !parsed.data.stream;
|
||||
const newTurnOutput = {
|
||||
stdout: stdout.substring(0, 10240), // Truncate to 10KB
|
||||
stderr: stderr.substring(0, 2048), // Truncate to 2KB
|
||||
truncated: stdout.length > 10240 || stderr.length > 2048
|
||||
stdout: stdout.substring(0, 10240), // Truncate preview to 10KB
|
||||
stderr: stderr.substring(0, 2048), // Truncate preview to 2KB
|
||||
truncated: stdout.length > 10240 || stderr.length > 2048,
|
||||
cached: shouldCache,
|
||||
stdout_full: shouldCache ? stdout : undefined,
|
||||
stderr_full: shouldCache ? stderr : undefined
|
||||
};
|
||||
|
||||
// Determine base turn number for merge scenarios
|
||||
@@ -994,19 +1069,24 @@ async function executeCliTool(
|
||||
reject(new Error(`Failed to spawn ${tool}: ${error.message}`));
|
||||
});
|
||||
|
||||
// Timeout handling
|
||||
const timeoutId = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill('SIGTERM');
|
||||
setTimeout(() => {
|
||||
if (!child.killed) {
|
||||
child.kill('SIGKILL');
|
||||
}
|
||||
}, 5000);
|
||||
}, timeout);
|
||||
// Timeout handling (timeout=0 disables internal timeout, controlled by external caller)
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
if (timeout > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill('SIGTERM');
|
||||
setTimeout(() => {
|
||||
if (!child.killed) {
|
||||
child.kill('SIGKILL');
|
||||
}
|
||||
}, 5000);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
child.on('close', () => {
|
||||
clearTimeout(timeoutId);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1051,8 +1131,8 @@ Modes:
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
description: 'Timeout in milliseconds (default: 300000 = 5 minutes)',
|
||||
default: 300000
|
||||
description: 'Timeout in milliseconds (default: 0 = disabled, controlled by external caller)',
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
required: ['tool', 'prompt']
|
||||
|
||||
@@ -23,6 +23,9 @@ export interface ConversationTurn {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
truncated: boolean;
|
||||
cached?: boolean;
|
||||
stdout_full?: string;
|
||||
stderr_full?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -315,6 +318,28 @@ export class CliHistoryStore {
|
||||
} catch (indexErr) {
|
||||
console.warn('[CLI History] Turns timestamp index creation warning:', (indexErr as Error).message);
|
||||
}
|
||||
|
||||
// Add cached output columns to turns table for non-streaming mode
|
||||
const turnsInfo = this.db.prepare('PRAGMA table_info(turns)').all() as Array<{ name: string }>;
|
||||
const hasCached = turnsInfo.some(col => col.name === 'cached');
|
||||
const hasStdoutFull = turnsInfo.some(col => col.name === 'stdout_full');
|
||||
const hasStderrFull = turnsInfo.some(col => col.name === 'stderr_full');
|
||||
|
||||
if (!hasCached) {
|
||||
console.log('[CLI History] Migrating database: adding cached column to turns table...');
|
||||
this.db.exec('ALTER TABLE turns ADD COLUMN cached INTEGER DEFAULT 0;');
|
||||
console.log('[CLI History] Migration complete: cached column added');
|
||||
}
|
||||
if (!hasStdoutFull) {
|
||||
console.log('[CLI History] Migrating database: adding stdout_full column to turns table...');
|
||||
this.db.exec('ALTER TABLE turns ADD COLUMN stdout_full TEXT;');
|
||||
console.log('[CLI History] Migration complete: stdout_full column added');
|
||||
}
|
||||
if (!hasStderrFull) {
|
||||
console.log('[CLI History] Migrating database: adding stderr_full column to turns table...');
|
||||
this.db.exec('ALTER TABLE turns ADD COLUMN stderr_full TEXT;');
|
||||
console.log('[CLI History] Migration complete: stderr_full column added');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[CLI History] Migration error:', (err as Error).message);
|
||||
// Don't throw - allow the store to continue working with existing schema
|
||||
@@ -421,8 +446,8 @@ export class CliHistoryStore {
|
||||
`);
|
||||
|
||||
const upsertTurn = this.db.prepare(`
|
||||
INSERT INTO turns (conversation_id, turn_number, timestamp, prompt, duration_ms, status, exit_code, stdout, stderr, truncated)
|
||||
VALUES (@conversation_id, @turn_number, @timestamp, @prompt, @duration_ms, @status, @exit_code, @stdout, @stderr, @truncated)
|
||||
INSERT INTO turns (conversation_id, turn_number, timestamp, prompt, duration_ms, status, exit_code, stdout, stderr, truncated, cached, stdout_full, stderr_full)
|
||||
VALUES (@conversation_id, @turn_number, @timestamp, @prompt, @duration_ms, @status, @exit_code, @stdout, @stderr, @truncated, @cached, @stdout_full, @stderr_full)
|
||||
ON CONFLICT(conversation_id, turn_number) DO UPDATE SET
|
||||
timestamp = @timestamp,
|
||||
prompt = @prompt,
|
||||
@@ -431,7 +456,10 @@ export class CliHistoryStore {
|
||||
exit_code = @exit_code,
|
||||
stdout = @stdout,
|
||||
stderr = @stderr,
|
||||
truncated = @truncated
|
||||
truncated = @truncated,
|
||||
cached = @cached,
|
||||
stdout_full = @stdout_full,
|
||||
stderr_full = @stderr_full
|
||||
`);
|
||||
|
||||
const transaction = this.db.transaction(() => {
|
||||
@@ -463,7 +491,10 @@ export class CliHistoryStore {
|
||||
exit_code: turn.exit_code,
|
||||
stdout: turn.output.stdout,
|
||||
stderr: turn.output.stderr,
|
||||
truncated: turn.output.truncated ? 1 : 0
|
||||
truncated: turn.output.truncated ? 1 : 0,
|
||||
cached: turn.output.cached ? 1 : 0,
|
||||
stdout_full: turn.output.stdout_full || null,
|
||||
stderr_full: turn.output.stderr_full || null
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -507,7 +538,10 @@ export class CliHistoryStore {
|
||||
output: {
|
||||
stdout: t.stdout || '',
|
||||
stderr: t.stderr || '',
|
||||
truncated: !!t.truncated
|
||||
truncated: !!t.truncated,
|
||||
cached: !!t.cached,
|
||||
stdout_full: t.stdout_full || undefined,
|
||||
stderr_full: t.stderr_full || undefined
|
||||
}
|
||||
}))
|
||||
};
|
||||
@@ -533,6 +567,92 @@ export class CliHistoryStore {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paginated cached output for a conversation turn
|
||||
* @param conversationId - Conversation ID
|
||||
* @param turnNumber - Turn number (default: latest turn)
|
||||
* @param options - Pagination options
|
||||
*/
|
||||
getCachedOutput(
|
||||
conversationId: string,
|
||||
turnNumber?: number,
|
||||
options: {
|
||||
offset?: number; // Character offset (default: 0)
|
||||
limit?: number; // Max characters to return (default: 10000)
|
||||
outputType?: 'stdout' | 'stderr' | 'both'; // Which output to fetch
|
||||
} = {}
|
||||
): {
|
||||
conversationId: string;
|
||||
turnNumber: number;
|
||||
stdout?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||
stderr?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||
cached: boolean;
|
||||
prompt: string;
|
||||
status: string;
|
||||
timestamp: string;
|
||||
} | null {
|
||||
const { offset = 0, limit = 10000, outputType = 'both' } = options;
|
||||
|
||||
// Get turn (latest if not specified)
|
||||
let turn;
|
||||
if (turnNumber !== undefined) {
|
||||
turn = this.db.prepare(`
|
||||
SELECT * FROM turns WHERE conversation_id = ? AND turn_number = ?
|
||||
`).get(conversationId, turnNumber) as any;
|
||||
} else {
|
||||
turn = this.db.prepare(`
|
||||
SELECT * FROM turns WHERE conversation_id = ? ORDER BY turn_number DESC LIMIT 1
|
||||
`).get(conversationId) as any;
|
||||
}
|
||||
|
||||
if (!turn) return null;
|
||||
|
||||
const result: {
|
||||
conversationId: string;
|
||||
turnNumber: number;
|
||||
stdout?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||
stderr?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
|
||||
cached: boolean;
|
||||
prompt: string;
|
||||
status: string;
|
||||
timestamp: string;
|
||||
} = {
|
||||
conversationId,
|
||||
turnNumber: turn.turn_number,
|
||||
cached: !!turn.cached,
|
||||
prompt: turn.prompt,
|
||||
status: turn.status,
|
||||
timestamp: turn.timestamp
|
||||
};
|
||||
|
||||
// Use full output if cached, otherwise use truncated
|
||||
if (outputType === 'stdout' || outputType === 'both') {
|
||||
const fullStdout = turn.cached ? (turn.stdout_full || '') : (turn.stdout || '');
|
||||
const totalBytes = fullStdout.length;
|
||||
const content = fullStdout.substring(offset, offset + limit);
|
||||
result.stdout = {
|
||||
content,
|
||||
totalBytes,
|
||||
offset,
|
||||
hasMore: offset + limit < totalBytes
|
||||
};
|
||||
}
|
||||
|
||||
if (outputType === 'stderr' || outputType === 'both') {
|
||||
const fullStderr = turn.cached ? (turn.stderr_full || '') : (turn.stderr || '');
|
||||
const totalBytes = fullStderr.length;
|
||||
const content = fullStderr.substring(offset, offset + limit);
|
||||
result.stderr = {
|
||||
content,
|
||||
totalBytes,
|
||||
offset,
|
||||
hasMore: offset + limit < totalBytes
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query execution history
|
||||
*/
|
||||
|
||||
@@ -33,6 +33,14 @@ const VENV_PYTHON =
|
||||
let bootstrapChecked = false;
|
||||
let bootstrapReady = false;
|
||||
|
||||
// Venv status cache with TTL
|
||||
interface VenvStatusCache {
|
||||
status: ReadyStatus;
|
||||
timestamp: number;
|
||||
}
|
||||
let venvStatusCache: VenvStatusCache | null = null;
|
||||
const VENV_STATUS_TTL = 5 * 60 * 1000; // 5 minutes TTL
|
||||
|
||||
// Track running indexing process for cancellation
|
||||
let currentIndexingProcess: ReturnType<typeof spawn> | null = null;
|
||||
let currentIndexingAborted = false;
|
||||
@@ -77,6 +85,7 @@ interface SemanticStatus {
|
||||
backend?: string;
|
||||
accelerator?: string;
|
||||
providers?: string[];
|
||||
litellmAvailable?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -115,6 +124,13 @@ interface ProgressInfo {
|
||||
totalFiles?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear venv status cache (call after install/uninstall operations)
|
||||
*/
|
||||
function clearVenvStatusCache(): void {
|
||||
venvStatusCache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect available Python 3 executable
|
||||
* @returns Python executable command
|
||||
@@ -137,17 +153,27 @@ function getSystemPython(): string {
|
||||
|
||||
/**
|
||||
* Check if CodexLens venv exists and has required packages
|
||||
* @param force - Force refresh cache (default: false)
|
||||
* @returns Ready status
|
||||
*/
|
||||
async function checkVenvStatus(): Promise<ReadyStatus> {
|
||||
async function checkVenvStatus(force = false): Promise<ReadyStatus> {
|
||||
// Use cached result if available and not expired
|
||||
if (!force && venvStatusCache && (Date.now() - venvStatusCache.timestamp < VENV_STATUS_TTL)) {
|
||||
return venvStatusCache.status;
|
||||
}
|
||||
|
||||
// Check venv exists
|
||||
if (!existsSync(CODEXLENS_VENV)) {
|
||||
return { ready: false, error: 'Venv not found' };
|
||||
const result = { ready: false, error: 'Venv not found' };
|
||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check python executable exists
|
||||
if (!existsSync(VENV_PYTHON)) {
|
||||
return { ready: false, error: 'Python executable not found in venv' };
|
||||
const result = { ready: false, error: 'Python executable not found in venv' };
|
||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check codexlens is importable
|
||||
@@ -168,15 +194,21 @@ async function checkVenvStatus(): Promise<ReadyStatus> {
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
let result: ReadyStatus;
|
||||
if (code === 0) {
|
||||
resolve({ ready: true, version: stdout.trim() });
|
||||
result = { ready: true, version: stdout.trim() };
|
||||
} else {
|
||||
resolve({ ready: false, error: `CodexLens not installed: ${stderr}` });
|
||||
result = { ready: false, error: `CodexLens not installed: ${stderr}` };
|
||||
}
|
||||
// Cache the result
|
||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
resolve({ ready: false, error: `Failed to check venv: ${err.message}` });
|
||||
const result = { ready: false, error: `Failed to check venv: ${err.message}` };
|
||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -198,8 +230,15 @@ async function checkSemanticStatus(): Promise<SemanticStatus> {
|
||||
import sys
|
||||
import json
|
||||
try:
|
||||
from codexlens.semantic import SEMANTIC_AVAILABLE, SEMANTIC_BACKEND
|
||||
result = {"available": SEMANTIC_AVAILABLE, "backend": SEMANTIC_BACKEND if SEMANTIC_AVAILABLE else None}
|
||||
import codexlens.semantic as semantic
|
||||
SEMANTIC_AVAILABLE = bool(getattr(semantic, "SEMANTIC_AVAILABLE", False))
|
||||
SEMANTIC_BACKEND = getattr(semantic, "SEMANTIC_BACKEND", None)
|
||||
LITELLM_AVAILABLE = bool(getattr(semantic, "LITELLM_AVAILABLE", False))
|
||||
result = {
|
||||
"available": SEMANTIC_AVAILABLE,
|
||||
"backend": SEMANTIC_BACKEND if SEMANTIC_AVAILABLE else None,
|
||||
"litellm_available": LITELLM_AVAILABLE,
|
||||
}
|
||||
|
||||
# Get ONNX providers for accelerator info
|
||||
try:
|
||||
@@ -250,6 +289,7 @@ except Exception as e:
|
||||
backend: result.backend,
|
||||
accelerator: result.accelerator || 'CPU',
|
||||
providers: result.providers || [],
|
||||
litellmAvailable: result.litellm_available || false,
|
||||
error: result.error
|
||||
});
|
||||
} catch {
|
||||
@@ -263,6 +303,77 @@ except Exception as e:
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure LiteLLM embedder dependencies are available in the CodexLens venv.
|
||||
* Installs ccw-litellm into the venv if needed.
|
||||
*/
|
||||
async function ensureLiteLLMEmbedderReady(): Promise<BootstrapResult> {
|
||||
// Ensure CodexLens venv exists and CodexLens is installed.
|
||||
const readyStatus = await ensureReady();
|
||||
if (!readyStatus.ready) {
|
||||
return { success: false, error: readyStatus.error || 'CodexLens not ready' };
|
||||
}
|
||||
|
||||
// Check if ccw_litellm can be imported
|
||||
const importStatus = await new Promise<{ ok: boolean; error?: string }>((resolve) => {
|
||||
const child = spawn(VENV_PYTHON, ['-c', 'import ccw_litellm; print("OK")'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve({ ok: code === 0, error: stderr.trim() || undefined });
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
resolve({ ok: false, error: err.message });
|
||||
});
|
||||
});
|
||||
|
||||
if (importStatus.ok) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const pipPath =
|
||||
process.platform === 'win32'
|
||||
? join(CODEXLENS_VENV, 'Scripts', 'pip.exe')
|
||||
: join(CODEXLENS_VENV, 'bin', 'pip');
|
||||
|
||||
try {
|
||||
console.log('[CodexLens] Installing ccw-litellm for LiteLLM embedding backend...');
|
||||
|
||||
const possiblePaths = [
|
||||
join(process.cwd(), 'ccw-litellm'),
|
||||
join(__dirname, '..', '..', '..', 'ccw-litellm'), // ccw/src/tools -> project root
|
||||
join(homedir(), 'ccw-litellm'),
|
||||
];
|
||||
|
||||
let installed = false;
|
||||
for (const localPath of possiblePaths) {
|
||||
if (existsSync(join(localPath, 'pyproject.toml'))) {
|
||||
console.log(`[CodexLens] Installing ccw-litellm from local path: ${localPath}`);
|
||||
execSync(`"${pipPath}" install -e "${localPath}"`, { stdio: 'inherit' });
|
||||
installed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!installed) {
|
||||
console.log('[CodexLens] Installing ccw-litellm from PyPI...');
|
||||
execSync(`"${pipPath}" install ccw-litellm`, { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { success: false, error: `Failed to install ccw-litellm: ${(err as Error).message}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GPU acceleration mode for semantic search
|
||||
*/
|
||||
@@ -421,6 +532,17 @@ async function installSemantic(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResul
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
// IMPORTANT: fastembed installs onnxruntime (CPU) as dependency, which conflicts
|
||||
// with onnxruntime-directml/gpu. Reinstall the GPU version to ensure it takes precedence.
|
||||
if (gpuMode !== 'cpu') {
|
||||
try {
|
||||
console.log(`[CodexLens] Reinstalling ${onnxPackage} to ensure GPU provider works...`);
|
||||
execSync(`"${pipPath}" install --force-reinstall ${onnxPackage}`, { stdio: 'pipe', timeout: 300000 });
|
||||
console.log(`[CodexLens] ${onnxPackage} reinstalled successfully`);
|
||||
} catch (e) {
|
||||
console.warn(`[CodexLens] Warning: Failed to reinstall ${onnxPackage}: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
console.log(`[CodexLens] Semantic dependencies installed successfully (${gpuMode} mode)`);
|
||||
resolve({ success: true, message: `Installed with ${modeDescription}` });
|
||||
} else {
|
||||
@@ -490,6 +612,8 @@ async function bootstrapVenv(): Promise<BootstrapResult> {
|
||||
execSync(`"${pipPath}" install codexlens`, { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
// Clear cache after successful installation
|
||||
clearVenvStatusCache();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { success: false, error: `Failed to install codexlens: ${(err as Error).message}` };
|
||||
@@ -1209,6 +1333,7 @@ async function uninstallCodexLens(): Promise<BootstrapResult> {
|
||||
// Reset bootstrap cache
|
||||
bootstrapChecked = false;
|
||||
bootstrapReady = false;
|
||||
clearVenvStatusCache();
|
||||
|
||||
console.log('[CodexLens] CodexLens uninstalled successfully');
|
||||
return { success: true, message: 'CodexLens uninstalled successfully' };
|
||||
@@ -1273,7 +1398,19 @@ function isIndexingInProgress(): boolean {
|
||||
export type { ProgressInfo, ExecuteOptions };
|
||||
|
||||
// Export for direct usage
|
||||
export { ensureReady, executeCodexLens, checkVenvStatus, bootstrapVenv, checkSemanticStatus, installSemantic, detectGpuSupport, uninstallCodexLens, cancelIndexing, isIndexingInProgress };
|
||||
export {
|
||||
ensureReady,
|
||||
executeCodexLens,
|
||||
checkVenvStatus,
|
||||
bootstrapVenv,
|
||||
checkSemanticStatus,
|
||||
ensureLiteLLMEmbedderReady,
|
||||
installSemantic,
|
||||
detectGpuSupport,
|
||||
uninstallCodexLens,
|
||||
cancelIndexing,
|
||||
isIndexingInProgress,
|
||||
};
|
||||
export type { GpuMode };
|
||||
|
||||
// Backward-compatible export for tests
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user