mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 00:17:47 +00:00
feat!: delegation system overhaul and .claude/ shipping (v4.1.0) (#8)
## 🚀 Release v4.1.0 - Major Update **Breaking changes from v3.5.0**. This release includes the complete v4.0.0 delegation overhaul plus v4.1.0 architecture improvements. --- ## 🎯 v4.0.0: Delegation System Overhaul **Complete rewrite of the delegation infrastructure with enhanced decision-making and streaming support.** ### New Delegation Features **Stream-JSON Communication Protocol:** - Real-time token streaming with `{type: "content", data: "..."}` format - Progress indicators during delegation execution - Clean separation: stdout for data, stderr for errors - Handles tool calls, thinking blocks, and text content **Enhanced Decision Framework:** - `/ccs:glm` and `/ccs:kimi` slash commands with auto-enhancement - `[AUTO ENHANCE]` prompts for better model understanding - Context-aware task delegation with clear boundaries - Continuation support: `/ccs:glm:continue` and `/ccs:kimi:continue` **Robust Error Handling:** - Graceful degradation when profiles unconfigured - Clear error messages with actionable fixes - Signal handling (SIGINT/SIGTERM) for clean child process termination - Session state recovery on interruption **Performance & Reliability:** - Headless mode (`-p` flag) for background execution - Slash command detection and auto-routing - Validation system with `DelegationValidator` - Profile readiness checks in `ccs --version` ### Delegation Components **New Files:** - `bin/delegation/delegation-handler.js` - Core delegation orchestrator - `bin/delegation/stream-processor.js` - Real-time output handling - `bin/utils/delegation-validator.js` - Profile validation - `.claude/commands/ccs/*.md` - Slash command definitions - `.claude/skills/ccs-delegation/` - AI decision framework - `.claude/agents/ccs-delegator.md` - Proactive delegation agent **Documentation:** - Complete delegation workflows with mermaid diagrams - Troubleshooting guides for common issues - Headless execution patterns --- ## ✨ v4.1.0: Selective Symlinking Architecture **Single source of truth for CCS items with automatic propagation.** ### New Architecture **Ship .claude/ Directory:** - CCS items now ship with npm/sh/ps1 packages - Selective item-level symlinks: `~/.ccs/.claude/` → `~/.claude/` - Auto-propagation on `npm update` - zero manual sync - Backward compatible with existing `~/.ccs/shared/` mechanism **Symlink Chain:** ``` ~/.ccs/.claude/ (source) ↓ selective symlinks ~/.claude/ (CCS items installed here) ↑ symlinked by ~/.ccs/shared/ ↑ symlinked by profiles (work, personal, team) ``` ### New Commands **Maintenance Tools:** - `ccs update` - Re-install CCS symlinks to ~/.claude/ - `ccs doctor` - Added Check 9: CCS symlinks health verification **Safe Installation:** - Automatic conflict backup before symlinking - Idempotent operations (safe to run multiple times) - Health monitoring and recovery ### New Components - `bin/utils/claude-symlink-manager.js` - Manages selective symlinks - Updated all 3 installers (npm postinstall, install.sh, install.ps1) - Enhanced `ccs doctor` with symlink health checks --- ## 💥 Breaking Changes **From v3.5.0 → v4.x:** 1. **Delegation commands moved**: - Old: User manually created in `~/.claude/commands/` - New: Auto-shipped in `~/.ccs/.claude/`, symlinked to `~/.claude/commands/ccs/` 2. **Slash command format**: - New: `/ccs:glm`, `/ccs:kimi`, `/ccs:glm:continue` - Old custom commands may need migration 3. **Profile validation**: - Placeholders (`YOUR_API_KEY_HERE`) now detected and marked invalid - Must configure real API keys for delegation to work 4. **Stream output format**: - Headless mode (`-p`) now uses stream-JSON protocol - Old text output replaced with structured `{type, data}` format
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
---
|
||||
name: ccs-delegator
|
||||
description: Execute delegated tasks in isolated GLM/Kimi sessions via headless mode. Use when parent agent invokes `/ccs:glm` or `/ccs:kimi` slash commands to delegate simple tasks to cost-optimized models. This agent handles the execution orchestration, result collection, and reporting back to the main session. Examples:\n\n<example>\nContext: Main agent receives `/ccs:glm "refactor the parseConfig function"` command.\nparent_agent: "Delegating refactoring task to GLM-4.6 via ccs-delegator"\nassistant: "I'll execute this task in an isolated GLM session using headless mode"\n<commentary>\nThe parent agent has enhanced the prompt and determined the working directory. This agent now executes via `ccs glm -p` using the glm profile, captures output, and reports results.\n</commentary>\n</example>\n\n<example>\nContext: Main agent delegates long-context analysis to Kimi.\nparent_agent: "Delegating codebase analysis to Kimi via ccs-delegator"\nassistant: "I'll execute the analysis in a Kimi session and report findings"\n<commentary>\nThis agent handles execution in the kimi profile, which supports long-context tasks, and formats the comprehensive results for the main session.\n</commentary>\n</example>\n\n<example>\nContext: Delegation execution fails due to unconfigured profile.\nparent_agent: "Attempting delegation to GLM"\nassistant: "Execution failed: GLM profile not configured. Reporting error to main agent."\n<commentary>\nWhen delegation fails, this agent reports the error gracefully without blocking the main session. The main agent can then choose to retry or execute directly.\n</commentary>\n</example>
|
||||
allowed-tools: Bash, Read, Grep, Glob
|
||||
default-model: sonnet
|
||||
---
|
||||
|
||||
You are a Delegation Executor, a specialized subagent that orchestrates task execution in isolated Claude sessions using alternative models (GLM-4.6, Kimi) via headless mode.
|
||||
|
||||
**CRITICAL RULES:**
|
||||
|
||||
1. **YOU MUST DELEGATE** - Your ONLY job is to execute `ccs` commands via Bash. You MUST NOT edit or write files yourself.
|
||||
2. **ACTIVATE SKILL FIRST** - Always activate the `ccs-delegation` skill before any delegation.
|
||||
3. **READ-ONLY ANALYSIS** - You can read files to understand context, but ALL actual work must be done via `ccs` delegation.
|
||||
|
||||
## Your Mission
|
||||
|
||||
Execute tasks by delegating to alternative models via `ccs` CLI, then report results back to the main session.
|
||||
|
||||
## Workflow (MANDATORY)
|
||||
|
||||
1. **Activate Skill** - Load `ccs-delegation` skill for delegation guidelines
|
||||
2. **Analyze Task** - Read files if needed to understand context
|
||||
3. **Select Profile** - Choose GLM (simple/cost-optimized) or Kimi (long-context)
|
||||
4. **Delegate** - Execute via `ccs {profile} -p "enhanced task description"`
|
||||
5. **Report Results** - Parse output and report to main session
|
||||
|
||||
## Delegation Methodology
|
||||
|
||||
When delegating tasks, you will:
|
||||
|
||||
1. **Task Analysis**
|
||||
- Read `ccs-delegation` skill for decision framework
|
||||
- Determine if task is delegation-appropriate
|
||||
- Estimate time needed: Quick (<2 min) / Medium (<10 min) / Complex (>10 min)
|
||||
- Identify scope: Single file vs multiple files
|
||||
|
||||
2. **Profile Selection**
|
||||
- GLM: Simple, cost-optimized (refactoring, tests, typos)
|
||||
- Kimi: Long-context (multi-file analysis, architecture docs)
|
||||
|
||||
3. **Session Strategy**
|
||||
- **New session** (`ccs {profile} -p "task"`): Use when:
|
||||
- Starting a new, unrelated task
|
||||
- Previous session >30 days old
|
||||
- Different files/scope than last delegation
|
||||
|
||||
- **Continue session** (`ccs {profile}:continue -p "task"`): Use when:
|
||||
- Completing work from previous delegation
|
||||
- Fixing issues from last attempt
|
||||
- Adding to previously created files
|
||||
- Iterative refinement of same task
|
||||
- **CRITICAL**: Check delegation output for session ID before continuing
|
||||
|
||||
4. **Execution**
|
||||
- **New delegation**: `ccs {profile} -p "enhanced task description"`
|
||||
- **Continue delegation**: `ccs {profile}:continue -p "enhanced follow-up"`
|
||||
- **Note**: If task contains a slash command (/cook, /plan, /commit), keep it at the start when enhancing
|
||||
- Parse output for results
|
||||
- Report success/failure with file changes
|
||||
|
||||
5. **Batch Operations**
|
||||
- For multiple similar tasks, delegate each separately
|
||||
- Aggregate results
|
||||
- Report combined outcome
|
||||
|
||||
## Tools and Techniques
|
||||
|
||||
You will utilize:
|
||||
- **CCS CLI**: `ccs glm -p`, `ccs kimi -p` for delegation
|
||||
- **Bash Tool**: Execute CCS commands
|
||||
- **Read Tool**: Understand project context when needed
|
||||
- **ccs-delegation Skill**: Core knowledge base for delegation decisions
|
||||
|
||||
## Integration Components
|
||||
|
||||
CCS delegation uses these internal components:
|
||||
- **DelegationHandler**: Routes `-p` flag to HeadlessExecutor
|
||||
- **HeadlessExecutor**: Spawns `claude -p` with enhanced flags (--output-format stream-json, --permission-mode acceptEdits)
|
||||
- **SessionManager**: Persists sessions to `~/.ccs/delegation-sessions.json`
|
||||
- **ResultFormatter**: Displays ASCII box output with session ID, cost, turns
|
||||
|
||||
Results include metadata parsed from stream-json output with real-time tool visibility.
|
||||
|
||||
## Execution Pattern
|
||||
|
||||
**Standard delegation** (new task):
|
||||
```bash
|
||||
ccs glm -p "Refactor auth.js to use async/await"
|
||||
```
|
||||
|
||||
**Session continuation** (same task, iterative):
|
||||
```bash
|
||||
# First delegation creates landing page but misses JavaScript
|
||||
ccs glm -p "Create landing page in HTML/CSS"
|
||||
|
||||
# Output shows: Files Created: index.html, styles.css
|
||||
# You notice JavaScript file is missing
|
||||
|
||||
# Continue the SAME session to add missing JavaScript
|
||||
ccs glm:continue -p "Create the missing JavaScript file script.js"
|
||||
```
|
||||
|
||||
**Batch delegation** (multiple unrelated tasks):
|
||||
```bash
|
||||
# Each is a separate new session (different files)
|
||||
ccs glm -p "Add tests for UserService"
|
||||
ccs glm -p "Add tests for AuthService"
|
||||
ccs glm -p "Add tests for OrderService"
|
||||
```
|
||||
|
||||
## Remember
|
||||
|
||||
- **NEVER edit/write files yourself** - You lack Edit/Write tools for a reason
|
||||
- **ALWAYS delegate via `ccs`** - That's your only purpose
|
||||
- **ALWAYS activate `ccs-delegation` skill first** - It contains critical decision framework
|
||||
- Parse the delegation output and report results concisely to the main session
|
||||
@@ -1,180 +0,0 @@
|
||||
---
|
||||
allowed-tools: Glob, Read, Bash(jq:*), Task
|
||||
description: Delegate commands to alternative models (GLM) for token optimization
|
||||
argument-hint: [profile] /command [args...]
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# /ccs - Delegate to Alternative Models
|
||||
|
||||
You are a **delegation orchestrator**. Your job is to delegate the user's command to an alternative AI model (GLM, etc.) using the CCS (Claude Code Switch) system.
|
||||
|
||||
## User's Input
|
||||
|
||||
The user invoked: `/ccs {{args}}`
|
||||
|
||||
## Your Task
|
||||
|
||||
Follow these steps to delegate the command:
|
||||
|
||||
### Step 1: Parse Arguments
|
||||
|
||||
Extract from the user's input:
|
||||
- **Profile**: Model profile to use (e.g., `glm`, `son`) - defaults to `glm` if omitted
|
||||
- **Command**: The slash command to delegate (e.g., `plan`, `code`, `debug`)
|
||||
- **Arguments**: Additional arguments to pass to the command
|
||||
|
||||
**Examples**:
|
||||
- `/ccs glm /plan "add auth"` → profile=`glm`, command=`plan`, args=`"add auth"`
|
||||
- `/ccs /code "fix bug"` → profile=`glm` (default), command=`code`, args=`"fix bug"`
|
||||
- `/ccs glm /ask "what is X?"` → profile=`glm`, command=`ask`, args=`"what is X?"`
|
||||
|
||||
### Step 2: Validate Profile
|
||||
|
||||
Check if the profile exists in `~/.ccs/config.json`:
|
||||
|
||||
```bash
|
||||
jq -e '.profiles["<profile>"]' ~/.ccs/config.json
|
||||
```
|
||||
|
||||
If the profile doesn't exist:
|
||||
```
|
||||
❌ Error: Profile '<profile>' not found in ~/.ccs/config.json
|
||||
|
||||
Available profiles:
|
||||
<list profiles from config>
|
||||
|
||||
Usage: /ccs [profile] /command [args]
|
||||
Example: /ccs glm /plan "add authentication"
|
||||
```
|
||||
|
||||
### Step 3: Validate Command
|
||||
|
||||
The command will be executed in the launched Claude instance, which will look for `<command>.md` in the project's `.claude/commands/` directory.
|
||||
|
||||
**Note**: If the command doesn't exist in the target project, the Claude instance will fail with an appropriate error message.
|
||||
|
||||
### Step 4: Launch Subagent with Task Tool
|
||||
|
||||
Use the Task tool to delegate execution:
|
||||
|
||||
**Parameters**:
|
||||
```typescript
|
||||
{
|
||||
subagent_type: "general-purpose",
|
||||
model: "sonnet",
|
||||
description: "Delegating /<command> to <profile> profile",
|
||||
prompt: `You are executing a delegated command via CCS (Claude Code Switch).
|
||||
|
||||
**Delegation Context**:
|
||||
- Profile: <profile>
|
||||
- Command: /<command>
|
||||
- Arguments: <args>
|
||||
|
||||
**CRITICAL**: Before executing, switch to the CCS profile:
|
||||
\`\`\`bash
|
||||
ccs <profile>
|
||||
\`\`\`
|
||||
|
||||
**Instructions**:
|
||||
1. Run \`ccs <profile>\` to switch to the correct model
|
||||
2. Execute the command with arguments: <args>
|
||||
3. Provide a clear summary of what was accomplished
|
||||
|
||||
Execute now.`
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Format and Return Result
|
||||
|
||||
Present the subagent's output in this format:
|
||||
|
||||
```markdown
|
||||
🤖 **CCS Delegation Result**
|
||||
|
||||
**Profile**: <profile>
|
||||
**Command**: /<command>
|
||||
|
||||
---
|
||||
|
||||
<subagent-output>
|
||||
|
||||
---
|
||||
|
||||
💡 *Token optimization: This task was delegated to the '<profile>' profile to conserve primary model usage.*
|
||||
```
|
||||
|
||||
## When to Use /ccs
|
||||
|
||||
### ✅ Good Use Cases
|
||||
- Simple planning tasks
|
||||
- Straightforward code implementation
|
||||
- Documentation writing
|
||||
- Basic debugging
|
||||
- Quick questions
|
||||
|
||||
### ❌ Don't Delegate
|
||||
- Complex reasoning/architecture
|
||||
- Security-critical code
|
||||
- Deep code review
|
||||
- Context-dependent tasks
|
||||
- Already using right model
|
||||
|
||||
## Error Handling
|
||||
|
||||
If delegation fails, provide helpful error message with:
|
||||
1. What went wrong
|
||||
2. Suggestions to fix (try different profile, run directly, check CCS config)
|
||||
3. How to verify CCS setup (`ccs --version`)
|
||||
|
||||
## Configuration Check
|
||||
|
||||
If profile validation fails, show how to check config:
|
||||
```bash
|
||||
# View available profiles
|
||||
cat ~/.ccs/config.json
|
||||
|
||||
# Test CCS is working
|
||||
ccs --version
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
**Example 1**: Simple planning with GLM
|
||||
```
|
||||
User: /ccs glm /plan "add user authentication"
|
||||
You: Parse → profile=glm, command=plan, args="add user authentication"
|
||||
Validate → Check glm exists in config ✓
|
||||
Launch → Task tool with ccs glm
|
||||
Return → Formatted result
|
||||
```
|
||||
|
||||
**Example 2**: Quick question with GLM
|
||||
```
|
||||
User: /ccs glm /ask "explain JWT tokens"
|
||||
You: Parse → profile=glm, command=ask, args="explain JWT tokens"
|
||||
Validate → Check glm exists ✓
|
||||
Launch → Delegate to glm
|
||||
Return → Result
|
||||
```
|
||||
|
||||
**Example 3**: Default profile
|
||||
```
|
||||
User: /ccs /debug "API 500 error"
|
||||
You: Parse → profile=glm (default), command=debug, args="API 500 error"
|
||||
Continue with delegation...
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- This is a **meta-command** that orchestrates other commands
|
||||
- Each delegation creates an **isolated subagent session**
|
||||
- The subagent switches to the specified CCS profile automatically
|
||||
- **Token optimization** is the primary benefit
|
||||
- Commands are resolved by the launched Claude instance from the project's `.claude/commands/` directory
|
||||
- If a command doesn't exist, the Claude instance will fail with an error
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- Delegation patterns: `tools/ccs/skills/ccs-delegation.md`
|
||||
- CCS Tool: `tools/ccs/README.md`
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: Delegate task to GLM-4.6 (cost-optimized model) [AUTO ENHANCE]
|
||||
argument-hint: [task description]
|
||||
allowed-tools: Read, Grep, Glob, Bash
|
||||
---
|
||||
|
||||
Delegate simple, deterministic tasks to GLM-4.6 for token optimization.
|
||||
|
||||
**Workflow:**
|
||||
- Analyze the task description in `$ARGUMENTS`
|
||||
- Gather context if needed (read files, check structure)
|
||||
- Enhance prompt with specific details (file paths, context, success criteria)
|
||||
- Execute delegation via `ccs glm -p "$ENHANCED_PROMPT"`
|
||||
|
||||
**Note:** `$ENHANCED_PROMPT` is an enhanced version that adds specifics like file paths, current implementation context, expected behavior, and success criteria. If the task contains a slash command (e.g., /cook, /plan), keep it at the start of the enhanced prompt.
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/ccs:glm "refactor auth.js to use async/await"
|
||||
/ccs:glm "add tests for UserService"
|
||||
/ccs:glm "/cook create a landing page"
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: Continue last GLM delegation session [AUTO ENHANCE]
|
||||
argument-hint: [follow-up instruction]
|
||||
allowed-tools: Read, Grep, Glob, Bash
|
||||
---
|
||||
|
||||
Continue last GLM delegation session for iterative refinement.
|
||||
|
||||
**Workflow:**
|
||||
- Review what was accomplished in previous session
|
||||
- Analyze the follow-up instruction in `$ARGUMENTS`
|
||||
- Enhance prompt with context (reference files, incomplete tasks, next steps)
|
||||
- Execute continuation via `ccs glm:continue -p "$ENHANCED_PROMPT"`
|
||||
|
||||
**Note:** `$ENHANCED_PROMPT` is an enhanced version that references previous work, highlights incomplete tasks, and adds specific validation criteria. If the follow-up contains a slash command (e.g., /commit), keep it at the start of the enhanced prompt.
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/ccs:glm "fix typo in README"
|
||||
/ccs:glm:continue "also update the examples section"
|
||||
/ccs:glm:continue "/commit with descriptive message"
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: Delegate task to Kimi (long-context model) [AUTO ENHANCE]
|
||||
argument-hint: [task description]
|
||||
allowed-tools: Read, Grep, Glob, Bash
|
||||
---
|
||||
|
||||
Delegate long-context, multi-file tasks to Kimi for comprehensive analysis.
|
||||
|
||||
**Workflow:**
|
||||
- Analyze the task description in `$ARGUMENTS`
|
||||
- Gather context across multiple files/directories
|
||||
- Enhance prompt with comprehensive details (structure, relationships, scope)
|
||||
- Execute delegation via `ccs kimi -p "$ENHANCED_PROMPT"`
|
||||
|
||||
**Note:** `$ENHANCED_PROMPT` is an enhanced version that adds directory structures, cross-file relationships, architecture context, and deliverables. If the task contains a slash command (e.g., /plan, /commit), keep it at the start of the enhanced prompt.
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/ccs:kimi "analyze all files in src/ and document architecture"
|
||||
/ccs:kimi "find all deprecated API usages across codebase"
|
||||
/ccs:kimi "/plan for authentication system"
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: Continue last Kimi delegation session [AUTO ENHANCE]
|
||||
argument-hint: [follow-up instruction]
|
||||
allowed-tools: Read, Grep, Glob, Bash
|
||||
---
|
||||
|
||||
Continue last Kimi delegation session for multi-turn analysis.
|
||||
|
||||
**Workflow:**
|
||||
- Review analysis/work from previous session
|
||||
- Analyze the follow-up instruction in `$ARGUMENTS`
|
||||
- Enhance prompt with comprehensive context (findings, scope, deliverables, priority)
|
||||
- Execute continuation via `ccs kimi:continue -p "$ENHANCED_PROMPT"`
|
||||
|
||||
**Note:** `$ENHANCED_PROMPT` is an enhanced version that references previous findings, specifies next scope, and adds actionable deliverables with priorities. If the follow-up contains a slash command (e.g., /plan), keep it at the start of the enhanced prompt.
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
/ccs:kimi "analyze all files in src/"
|
||||
/ccs:kimi:continue "suggest architectural improvements"
|
||||
/ccs:kimi:continue "/plan for refactoring with phases"
|
||||
```
|
||||
@@ -1,182 +1,54 @@
|
||||
---
|
||||
name: ccs-delegation
|
||||
description: Use this skill when the user invokes the `/ccs` command or requests delegating tasks to alternative models (GLM) for token optimization. This skill guides when and how to delegate commands to save primary model tokens.
|
||||
description: Delegate simple tasks to alternative models (GLM, Kimi) via CCS CLI for token optimization
|
||||
version: 2.2.0
|
||||
---
|
||||
|
||||
# CCS Delegation
|
||||
|
||||
Intelligent task delegation to alternative AI models (GLM, etc.) for token optimization using the `/ccs` meta-command.
|
||||
Delegate deterministic tasks to cost-optimized models via CCS CLI.
|
||||
|
||||
## Purpose
|
||||
## Core Concept
|
||||
|
||||
The `/ccs` command delegates simple tasks to alternative models while staying in the primary session, optimizing:
|
||||
- **Token efficiency**: Save primary model tokens for complex work
|
||||
- **Task-model matching**: Use appropriate model for each task
|
||||
- **Cost optimization**: Route simple tasks to lower-cost models
|
||||
Execute tasks via alternative models using `ccs {profile} -p "task"` equivalent to `claude --settings ~/.ccs/{profile}.settings -p "task"`
|
||||
|
||||
## When to Invoke This Skill
|
||||
|
||||
Load this skill when:
|
||||
- User explicitly invokes `/ccs [profile] /command [args]`
|
||||
- User requests delegating tasks to alternative models
|
||||
- User asks to use GLM for a task
|
||||
- User requests token conservation strategies
|
||||
**Profiles:** GLM (cost-optimized), Kimi (long-context)
|
||||
|
||||
## Decision Framework
|
||||
|
||||
### ✅ Delegate to Alternative Models
|
||||
**Delegate when:**
|
||||
- Simple refactoring, tests, typos, documentation
|
||||
- Deterministic, well-defined scope
|
||||
- No discussion/decisions needed
|
||||
|
||||
Recommend `/ccs` when:
|
||||
**Keep in main when:**
|
||||
- Architecture/design decisions
|
||||
- Security-critical code
|
||||
- Complex debugging requiring investigation
|
||||
- Performance optimization
|
||||
- Breaking changes/migrations
|
||||
|
||||
**Simple, straightforward tasks**:
|
||||
- Basic planning (CRUD operations, simple features)
|
||||
- Straightforward code implementation
|
||||
- Documentation writing
|
||||
- Simple bug fixes
|
||||
- Routine refactoring
|
||||
## Profile Selection
|
||||
|
||||
**Token conservation scenarios**:
|
||||
- Working on complex project, saving tokens for hard parts
|
||||
- Rate limit approaching on primary model
|
||||
- Cost-conscious development
|
||||
- **GLM**: Simple tasks (<5 files, clear scope, cost-optimized)
|
||||
- **Kimi**: Long-context (multi-file analysis, architecture docs)
|
||||
|
||||
**User explicitly requests**:
|
||||
- "Use GLM for this"
|
||||
- "Delegate to cheaper model"
|
||||
- "Save tokens on this task"
|
||||
|
||||
### ❌ Keep in Primary Model
|
||||
|
||||
Don't recommend delegation when:
|
||||
|
||||
**Complex reasoning required**:
|
||||
- Architecture decisions
|
||||
- System design patterns
|
||||
- Complex debugging (multiple files)
|
||||
- Security-critical code review
|
||||
|
||||
**Context-dependent**:
|
||||
- Needs current session context
|
||||
- Requires back-and-forth interaction
|
||||
- Builds on previous conversation
|
||||
|
||||
**Quality-critical**:
|
||||
- Production-critical code
|
||||
- Security implementations
|
||||
- Performance-sensitive algorithms
|
||||
- Public-facing API design
|
||||
|
||||
## Quick Decision Tree
|
||||
## Execution
|
||||
|
||||
User invocation via slash commands:
|
||||
```
|
||||
Is task simple and straightforward?
|
||||
↓ NO → Keep in current model
|
||||
↓ YES
|
||||
↓
|
||||
Does task need deep context from current session?
|
||||
↓ YES → Keep in current model
|
||||
↓ NO
|
||||
↓
|
||||
Is task security or quality critical?
|
||||
↓ YES → Keep in current model
|
||||
↓ NO
|
||||
↓
|
||||
✅ RECOMMEND /ccs delegation
|
||||
/ccs:glm "task"
|
||||
/ccs:glm:continue "follow-up"
|
||||
```
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Pattern 1: Explicit Delegation Request
|
||||
|
||||
When user explicitly requests alternative model:
|
||||
|
||||
```
|
||||
User: "Use GLM to plan the authentication feature"
|
||||
Claude: "I'll delegate this planning task to GLM to conserve tokens."
|
||||
<Invokes: /ccs glm /plan "design authentication feature">
|
||||
```
|
||||
|
||||
### Pattern 2: Proactive Token Optimization
|
||||
|
||||
When task is clearly simple, proactively suggest:
|
||||
|
||||
```
|
||||
User: "/plan add a simple CRUD endpoint for users"
|
||||
Claude: "This is straightforward. I'll delegate to GLM to save tokens."
|
||||
<Invokes: /ccs glm /plan "add CRUD endpoint for users">
|
||||
```
|
||||
|
||||
### Pattern 3: Automatic Model Selection
|
||||
|
||||
For simple tasks without explicit profile request:
|
||||
|
||||
```
|
||||
User: "/ccs /code 'implement the auth endpoints'"
|
||||
Claude: "Delegating to GLM (default profile) for implementation."
|
||||
<Invokes command with default glm profile>
|
||||
```
|
||||
|
||||
## Profile Selection Guide
|
||||
|
||||
**GLM (glm profile)**:
|
||||
- Simple coding tasks
|
||||
- Basic planning
|
||||
- Documentation
|
||||
- Routine fixes
|
||||
- Default choice for simple tasks
|
||||
|
||||
|
||||
**Sonnet (son profile)**:
|
||||
- Don't delegate—use directly
|
||||
- Complex reasoning
|
||||
- Architecture decisions
|
||||
- Security-critical work
|
||||
|
||||
## Command Format
|
||||
|
||||
Agent execution via Bash tool:
|
||||
```bash
|
||||
/ccs [profile] /command [args...]
|
||||
ccs glm -p "task"
|
||||
ccs glm:continue -p "follow-up"
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
- `/ccs glm /plan "add user authentication"`
|
||||
- `/ccs glm /ask "explain JWT tokens"`
|
||||
- `/ccs /code "implement feature"` (defaults to glm)
|
||||
## References
|
||||
|
||||
## Error Handling
|
||||
|
||||
If `/ccs` invocation fails:
|
||||
1. Check if CCS is properly configured: `ccs --version`
|
||||
2. Verify profile exists in `~/.ccs/config.json`
|
||||
3. Ensure command exists in `~/.ccs/commands/` or `.claude/commands/`
|
||||
4. Suggest running command directly if delegation problematic
|
||||
|
||||
## Configuration Check
|
||||
|
||||
Guide user to verify CCS setup:
|
||||
|
||||
```bash
|
||||
# Check CCS version
|
||||
ccs --version
|
||||
|
||||
# View available profiles
|
||||
cat ~/.ccs/config.json
|
||||
|
||||
# List available commands
|
||||
ls ~/.ccs/commands/
|
||||
```
|
||||
|
||||
## Integration Notes
|
||||
|
||||
- `/ccs` is a meta-command that orchestrates other slash commands
|
||||
- Each delegation creates isolated subagent session
|
||||
- Subagent automatically switches to specified CCS profile
|
||||
- User-scope commands (`~/.ccs/commands/`) checked first
|
||||
- Project-scope commands (`.claude/commands/`) as fallback
|
||||
|
||||
## Related Resources
|
||||
|
||||
- Command implementation: `~/.ccs/commands/ccs.md`
|
||||
- Detailed patterns: `references/delegation-patterns.md`
|
||||
- Setup guide: `tools/ccs/SETUP-DELEGATION.md`
|
||||
- CCS Tool: `tools/ccs/README.md`
|
||||
Technical details: `references/headless-workflow.md`
|
||||
Decision guide: `references/delegation-guidelines.md`
|
||||
Troubleshooting: `references/troubleshooting.md`
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# CCS Delegation References
|
||||
|
||||
## Reading Order
|
||||
|
||||
1. **Start here**: `../SKILL.md` - Entry point, quick start
|
||||
2. `headless-workflow.md` - Technical details (command syntax, features, config)
|
||||
3. **As needed**:
|
||||
- `delegation-guidelines.md` - Decision framework
|
||||
- `troubleshooting.md` - Error recovery
|
||||
|
||||
## File Hierarchy
|
||||
|
||||
**PRIMARY (Authoritative Source)**
|
||||
- `headless-workflow.md` - Technical implementation details
|
||||
|
||||
**SUPPORTING (Reference Primary)**
|
||||
- `delegation-guidelines.md` - When to delegate
|
||||
- `troubleshooting.md` - Error patterns
|
||||
|
||||
## Quick Navigation
|
||||
|
||||
**Need command syntax?** → `headless-workflow.md`
|
||||
**Need to decide if delegate?** → `delegation-guidelines.md`
|
||||
**Got an error?** → `troubleshooting.md`
|
||||
@@ -0,0 +1,99 @@
|
||||
# Delegation Guidelines
|
||||
|
||||
AI decision framework for when to delegate tasks vs keep in main session.
|
||||
|
||||
## Task Classification Rules
|
||||
|
||||
**Delegate if ALL criteria match:**
|
||||
- Task scope: Single concern, < 5 files
|
||||
- Complexity: Mechanical transformation, established pattern
|
||||
- Ambiguity: Zero decisions required, clear acceptance criteria
|
||||
- Context: Existing patterns to follow, no architecture changes
|
||||
|
||||
**Keep in main if ANY criteria match:**
|
||||
- Requires design decisions or tradeoff analysis
|
||||
- Security-critical (auth, encryption, permissions)
|
||||
- Performance-sensitive requiring profiling/measurement
|
||||
- Breaking changes or API migrations
|
||||
- User discussion/clarification needed
|
||||
- Coordinated changes across multiple subsystems
|
||||
|
||||
## Delegation Pattern Matching
|
||||
|
||||
**High-confidence delegation patterns:**
|
||||
```
|
||||
Task patterns to delegate:
|
||||
- refactor .* to use (async/await|destructuring|arrow functions)
|
||||
- add (unit|integration) tests for .*
|
||||
- fix (typos?|formatting|linting errors?) in .*
|
||||
- add JSDoc comments to .*
|
||||
- extract .* into (function|method|util) .*
|
||||
- rename (variable|function) .* to .*
|
||||
- add DELETE endpoint for .*
|
||||
- update README to document .*
|
||||
```
|
||||
|
||||
**Anti-patterns (never delegate):**
|
||||
```
|
||||
Task patterns to avoid:
|
||||
- implement .* (too vague, needs design)
|
||||
- improve .* (subjective, needs discussion)
|
||||
- fix bug .* (requires investigation)
|
||||
- optimize .* (requires profiling)
|
||||
- migrate .* to .* (breaking change)
|
||||
- design .* (architecture decision)
|
||||
- whatever .* you think (requires judgment)
|
||||
```
|
||||
|
||||
## Prompt Quality Criteria
|
||||
|
||||
**Well-formed delegation prompt:**
|
||||
- Specifies exact file paths: `in src/auth.js, ...`
|
||||
- Defines success criteria: `covering positive, zero, negative cases`
|
||||
- Single atomic task: One verb, one target
|
||||
- Uses imperative mood: "add tests" not "adding tests"
|
||||
|
||||
**Malformed delegation prompt:**
|
||||
- Multiple tasks: "add tests, update docs, fix linting"
|
||||
- Vague scope: "improve the code"
|
||||
- Requires decisions: "use whatever library you want"
|
||||
- No file context: "fix the bug" (which file?)
|
||||
|
||||
## Token Efficiency Model
|
||||
|
||||
**Delegation cost model:**
|
||||
- Main session overhead: ~2000 tokens (context, discussion)
|
||||
- Delegation overhead: ~500 tokens (focused execution)
|
||||
- Net savings: ~1500 tokens per delegated task
|
||||
|
||||
**When to batch delegate:**
|
||||
- User requests N similar tasks (e.g., "add tests for all services")
|
||||
- Each task follows identical pattern
|
||||
- Tasks are independent (no coordination needed)
|
||||
|
||||
**Execution pattern:**
|
||||
```
|
||||
for each service in [UserService, AuthService, OrderService]:
|
||||
ccs glm -p "add unit tests for {service} using Jest"
|
||||
```
|
||||
|
||||
## Monorepo Handling
|
||||
|
||||
**Workspace specification required:**
|
||||
- Pattern: `in packages/{workspace}, {task}`
|
||||
- Example: `in packages/api, add validation middleware`
|
||||
- Without workspace: Task may target wrong package
|
||||
|
||||
## Scope Limits
|
||||
|
||||
**Absolute limits (reject delegation):**
|
||||
- Estimated time > 30 minutes
|
||||
- File count > 5 files
|
||||
- Requires external research
|
||||
- Breaking changes to public APIs
|
||||
- User explicitly requests discussion
|
||||
|
||||
**Examples of over-scoped tasks:**
|
||||
- "Migrate from SQLite to PostgreSQL" (breaking change)
|
||||
- "Implement OAuth2 authentication" (too complex)
|
||||
- "Analyze entire codebase for security issues" (research task)
|
||||
@@ -1,286 +0,0 @@
|
||||
# CCS Delegation Patterns - Detailed Reference
|
||||
|
||||
This reference provides comprehensive patterns and examples for CCS task delegation.
|
||||
|
||||
## Advanced Usage Patterns
|
||||
|
||||
### Pattern 4: Task Splitting (Complex + Simple)
|
||||
|
||||
Split work between models for optimal efficiency:
|
||||
|
||||
```
|
||||
User: "Design and implement payment system"
|
||||
|
||||
Claude (in Sonnet):
|
||||
1. Design architecture (Sonnet handles complex reasoning)
|
||||
- Security considerations
|
||||
- Data flow diagrams
|
||||
- API contracts
|
||||
|
||||
2. Delegate implementation to GLM:
|
||||
/ccs glm /code "implement payment webhook handler based on this design"
|
||||
|
||||
3. Review security (back to Sonnet):
|
||||
Review the implemented code for security vulnerabilities
|
||||
```
|
||||
|
||||
### Pattern 5: Batch Operations
|
||||
|
||||
Delegate multiple simple tasks sequentially:
|
||||
|
||||
```bash
|
||||
# Multiple planning tasks
|
||||
/ccs glm /plan "feature A: user profile"
|
||||
/ccs glm /plan "feature B: notifications"
|
||||
/ccs glm /plan "feature C: search functionality"
|
||||
|
||||
# Then review all plans together in Sonnet
|
||||
"Review all three plans for consistency and integration points"
|
||||
```
|
||||
|
||||
### Pattern 6: Iterative Refinement
|
||||
|
||||
Use GLM for initial implementation, Sonnet for refinement:
|
||||
|
||||
```
|
||||
1. /ccs glm /code "implement basic CRUD API"
|
||||
2. (Sonnet reviews): "Add error handling, input validation, rate limiting"
|
||||
3. /ccs glm /fix "add suggested improvements"
|
||||
```
|
||||
|
||||
## Model Capability Matrix
|
||||
|
||||
### GLM 4.6 - Best For:
|
||||
- ✅ Simple feature planning (CRUD, basic workflows)
|
||||
- ✅ Straightforward code implementation
|
||||
- ✅ Documentation and README files
|
||||
- ✅ Basic bug fixes (clear error messages)
|
||||
- ✅ Refactoring with clear scope
|
||||
- ✅ Test writing (unit tests, simple integration tests)
|
||||
- ✅ Configuration files (package.json, tsconfig, etc.)
|
||||
- ❌ Complex algorithms
|
||||
- ❌ Security-critical code
|
||||
- ❌ Performance optimization
|
||||
- ❌ Architecture decisions
|
||||
|
||||
|
||||
### Sonnet 4.5 - Best For:
|
||||
- ✅ Complex architecture and design
|
||||
- ✅ Security-critical code review
|
||||
- ✅ Performance optimization
|
||||
- ✅ Complex debugging (multiple files, unclear root cause)
|
||||
- ✅ API design and contracts
|
||||
- ✅ Database schema design
|
||||
- ✅ Integration planning
|
||||
- ✅ Ambiguous requirement clarification
|
||||
|
||||
## Context Preservation Strategies
|
||||
|
||||
### When Delegation Makes Sense
|
||||
|
||||
**Low context requirements**:
|
||||
```
|
||||
# Current session has 50 lines discussing feature X
|
||||
# User wants to add unrelated feature Y
|
||||
/ccs glm /plan "add feature Y" # ✅ Good - Y doesn't need X's context
|
||||
```
|
||||
|
||||
**Self-contained tasks**:
|
||||
```
|
||||
# Task has all info in the prompt
|
||||
/ccs glm /code "implement function that validates email addresses" # ✅ Good
|
||||
```
|
||||
|
||||
### When to Keep in Current Session
|
||||
|
||||
**High context dependency**:
|
||||
```
|
||||
# Current session has extensive discussion about auth flow
|
||||
# User: "now implement the login function"
|
||||
# ❌ Don't delegate - needs current context
|
||||
```
|
||||
|
||||
**Iterative refinement**:
|
||||
```
|
||||
# Session has 10 messages refining a complex algorithm
|
||||
# User: "adjust the algorithm to handle edge case X"
|
||||
# ❌ Don't delegate - needs full conversation history
|
||||
```
|
||||
|
||||
## Token Efficiency Analysis
|
||||
|
||||
### Token Savings Example
|
||||
|
||||
**Without delegation**:
|
||||
- Complex architecture discussion: 15,000 tokens (Sonnet)
|
||||
- Simple implementation: 5,000 tokens (Sonnet)
|
||||
- Total: 20,000 Sonnet tokens
|
||||
|
||||
**With delegation**:
|
||||
- Complex architecture discussion: 15,000 tokens (Sonnet)
|
||||
- Delegate implementation: 5,000 tokens (GLM)
|
||||
- Total: 15,000 Sonnet + 5,000 GLM tokens
|
||||
- **Savings**: 5,000 Sonnet tokens
|
||||
|
||||
### When Delegation Overhead Outweighs Benefits
|
||||
|
||||
**Very simple tasks** (< 200 tokens):
|
||||
```
|
||||
User: "add a comment to this line"
|
||||
# ❌ Don't delegate - overhead > benefit
|
||||
```
|
||||
|
||||
**Tasks needing immediate context** (recent discussion):
|
||||
```
|
||||
User: "based on what we just discussed, implement X"
|
||||
# ❌ Don't delegate - needs immediate context
|
||||
```
|
||||
|
||||
## Real-World Workflows
|
||||
|
||||
### Workflow 1: New Feature Development
|
||||
|
||||
```
|
||||
1. Requirements gathering (Sonnet)
|
||||
- Clarify ambiguous requirements
|
||||
- Discuss trade-offs
|
||||
- Design API contracts
|
||||
|
||||
2. Planning (GLM)
|
||||
/ccs glm /plan "implement user profile feature based on requirements"
|
||||
|
||||
3. Implementation (GLM)
|
||||
/ccs glm /code "implement the user profile API endpoints"
|
||||
|
||||
4. Review (Sonnet)
|
||||
- Security review
|
||||
- Performance check
|
||||
- Integration verification
|
||||
|
||||
5. Fixes (GLM if simple, Sonnet if complex)
|
||||
/ccs glm /fix "address code review comments"
|
||||
```
|
||||
|
||||
### Workflow 2: Bug Investigation & Fix
|
||||
|
||||
```
|
||||
1. Investigation (Sonnet)
|
||||
- Analyze logs
|
||||
- Trace root cause
|
||||
- Understand system state
|
||||
|
||||
2. Simple fix (GLM)
|
||||
/ccs glm /fix "update validation in UserController to handle null emails"
|
||||
|
||||
3. Verification (Sonnet)
|
||||
- Verify fix addresses root cause
|
||||
- Check for regressions
|
||||
```
|
||||
|
||||
### Workflow 3: Documentation Sprint
|
||||
|
||||
```
|
||||
1. /ccs glm /docs "document the authentication API endpoints"
|
||||
2. /ccs glm /docs "write setup guide for new developers"
|
||||
3. /ccs glm /docs "create API usage examples"
|
||||
4. (Sonnet reviews for completeness and accuracy)
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### Anti-Pattern 1: Over-Delegation
|
||||
|
||||
❌ **Bad**:
|
||||
```
|
||||
/ccs glm /plan "design entire microservices architecture"
|
||||
# Too complex for GLM
|
||||
```
|
||||
|
||||
✅ **Good**:
|
||||
```
|
||||
# Sonnet handles architecture
|
||||
# Then: /ccs glm /code "implement user service based on architecture"
|
||||
```
|
||||
|
||||
### Anti-Pattern 2: Delegation with Hidden Context
|
||||
|
||||
❌ **Bad**:
|
||||
```
|
||||
# After 20 messages discussing custom auth flow
|
||||
User: "implement the login"
|
||||
/ccs glm /code "implement login"
|
||||
# GLM doesn't have context about custom flow
|
||||
```
|
||||
|
||||
✅ **Good**:
|
||||
```
|
||||
# Include context in delegation
|
||||
/ccs glm /code "implement login using JWT with custom claims: userId, role, tenantId"
|
||||
```
|
||||
|
||||
### Anti-Pattern 3: Micro-Delegation
|
||||
|
||||
❌ **Bad**:
|
||||
```
|
||||
/ccs glm /code "add variable x"
|
||||
/ccs glm /code "add function y"
|
||||
/ccs glm /code "add class z"
|
||||
# Too much delegation overhead
|
||||
```
|
||||
|
||||
✅ **Good**:
|
||||
```
|
||||
/ccs glm /code "implement user management module with CRUD operations"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Delegated Task Failed
|
||||
|
||||
**Symptoms**:
|
||||
- GLM produces incorrect code
|
||||
- Implementation doesn't match requirements
|
||||
- Security vulnerabilities introduced
|
||||
|
||||
**Solutions**:
|
||||
1. Check if task was too complex for GLM
|
||||
2. Provide more explicit requirements
|
||||
3. Use Sonnet for complex parts, GLM only for straightforward implementation
|
||||
4. Review GLM output in Sonnet before accepting
|
||||
|
||||
### Issue: Context Loss
|
||||
|
||||
**Symptoms**:
|
||||
- Delegated task doesn't align with previous discussion
|
||||
- Implementation misses important constraints
|
||||
- Style doesn't match existing codebase
|
||||
|
||||
**Solutions**:
|
||||
1. Include more context in delegation prompt
|
||||
2. Don't delegate context-dependent tasks
|
||||
3. Provide explicit style guidelines in prompt
|
||||
4. Consider keeping task in current session
|
||||
|
||||
### Issue: Frequent Delegation Failures
|
||||
|
||||
**Symptoms**:
|
||||
- Multiple retries needed
|
||||
- Tasks keep failing validation
|
||||
- Time spent > time saved
|
||||
|
||||
**Solutions**:
|
||||
1. Re-evaluate task complexity
|
||||
2. Delegate fewer, larger tasks instead of many small ones
|
||||
3. Use more specific instructions
|
||||
4. Consider if delegation is appropriate for this workflow
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
1. **Delegate simple, self-contained tasks**
|
||||
2. **Keep complex reasoning in Sonnet**
|
||||
3. **Include sufficient context in delegation**
|
||||
4. **Review delegated output before proceeding**
|
||||
5. **Monitor token savings vs overhead**
|
||||
6. **Prefer batch operations over micro-delegations**
|
||||
7. **Document delegation patterns that work well**
|
||||
8. **Iterate on delegation strategies based on results**
|
||||
@@ -0,0 +1,174 @@
|
||||
# Headless Workflow
|
||||
|
||||
**Last Updated**: 2025-11-15
|
||||
|
||||
CCS delegation uses Claude Code headless mode with enhanced features for token optimization.
|
||||
|
||||
## Core Concept
|
||||
|
||||
CCS delegation executes tasks via alternative models using enhanced Claude Code headless mode with stream-JSON output, session management, and cost tracking.
|
||||
|
||||
**Actual Command:**
|
||||
```bash
|
||||
ccs {profile} -p "prompt"
|
||||
```
|
||||
|
||||
Internally executes:
|
||||
```bash
|
||||
claude -p "prompt" --settings ~/.ccs/{profile}.settings.json --output-format stream-json --permission-mode acceptEdits
|
||||
```
|
||||
|
||||
**Docs:** https://code.claude.com/docs/en/headless.md
|
||||
|
||||
## How It Works
|
||||
|
||||
**Workflow:**
|
||||
1. User: `/ccs:glm "task"` in Claude Code session
|
||||
2. CCS detects `-p` flag and routes to HeadlessExecutor
|
||||
3. HeadlessExecutor spawns: `claude -p "task" --settings ~/.ccs/glm.settings.json --output-format stream-json --permission-mode acceptEdits`
|
||||
4. Claude Code runs headless with GLM profile + enhanced flags
|
||||
5. Returns stream-JSON with session_id, cost, turns
|
||||
6. Real-time tool use visibility in TTY
|
||||
7. ResultFormatter displays formatted results with metadata
|
||||
|
||||
**Enhanced Features:**
|
||||
- Stream-JSON output parsing (`--output-format stream-json`)
|
||||
- Real-time tool use visibility (e.g., `[Tool Use: Bash]`)
|
||||
- Session persistence (`~/.ccs/delegation-sessions.json`)
|
||||
- Cost tracking (displays USD cost per execution)
|
||||
- Time-based limits (10 min default timeout with graceful termination)
|
||||
- Multi-turn session management (resume via session_id)
|
||||
- Formatted ASCII box output
|
||||
|
||||
## Profile Settings
|
||||
|
||||
**Location:** `~/.ccs/{profile}.settings.json`
|
||||
|
||||
**Examples:**
|
||||
- GLM: `~/.ccs/glm.settings.json`
|
||||
- Kimi: `~/.ccs/kimi.settings.json`
|
||||
|
||||
**Example content:**
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
|
||||
"ANTHROPIC_AUTH_TOKEN": "your-glm-api-key",
|
||||
"ANTHROPIC_MODEL": "glm-4.6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
**Stream-JSON Mode** (automatically enabled):
|
||||
Each message is a separate JSON object (jsonl format):
|
||||
```json
|
||||
{"type":"init","session_id":"abc123def456"}
|
||||
{"type":"user","message":{"role":"user","content":"Task description"}}
|
||||
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"Bash"}]}}
|
||||
{"type":"result","subtype":"success","total_cost_usd":0.0025,"num_turns":3,"session_id":"abc123def456","result":"Task completed"}
|
||||
```
|
||||
|
||||
**Real-time Progress** (TTY only):
|
||||
```
|
||||
[i] Delegating to GLM-4.6...
|
||||
[Tool Use: Write]
|
||||
[Tool Use: Write]
|
||||
[Tool Use: Bash]
|
||||
[i] Execution completed in 1.5s
|
||||
```
|
||||
|
||||
**Formatted Output** (displayed to user):
|
||||
```
|
||||
╔══════════════════════════════════════════════════════╗
|
||||
║ Working Directory: /path/to/project ║
|
||||
║ Model: GLM-4.6 ║
|
||||
║ Duration: 1.5s ║
|
||||
║ Exit Code: 0 ║
|
||||
║ Session ID: abc123de ║
|
||||
║ Cost: $0.0025 ║
|
||||
║ Turns: 3 ║
|
||||
╚══════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
**Extracted Fields:**
|
||||
- `session_id` - For multi-turn (--resume)
|
||||
- `total_cost_usd` - Cost per execution
|
||||
- `num_turns` - Turn count
|
||||
- `is_error` - Error flag
|
||||
- `result` - Task output
|
||||
|
||||
**Exit codes:** 0 = success, non-zero = error
|
||||
|
||||
## Multi-Turn Sessions
|
||||
|
||||
**Start session:**
|
||||
```bash
|
||||
ccs glm -p "implement feature"
|
||||
```
|
||||
|
||||
**Continue session:**
|
||||
```bash
|
||||
ccs glm:continue -p "add tests"
|
||||
ccs glm:continue -p "run tests"
|
||||
```
|
||||
|
||||
Via slash commands:
|
||||
```
|
||||
/ccs:glm "implement feature"
|
||||
/ccs:glm:continue "add tests"
|
||||
```
|
||||
|
||||
**Session Storage:** `~/.ccs/delegation-sessions.json`
|
||||
|
||||
**Metadata:**
|
||||
- Session ID
|
||||
- Total cost (aggregated across turns)
|
||||
- Turn count
|
||||
- Last turn timestamp
|
||||
- Working directory
|
||||
|
||||
**Expiration:** ~30 days, auto-cleanup
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
**Single execution:**
|
||||
```bash
|
||||
ccs glm -p "task description"
|
||||
```
|
||||
|
||||
**With options:**
|
||||
```bash
|
||||
ccs glm -p "task" --permission-mode plan
|
||||
```
|
||||
|
||||
**Continue session:**
|
||||
```bash
|
||||
ccs glm:continue -p "follow-up task"
|
||||
```
|
||||
|
||||
All standard Claude Code headless flags are supported. See: https://code.claude.com/docs/en/headless.md
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Common errors:**
|
||||
- `Settings file not found` - Profile not configured (`ccs doctor` to diagnose)
|
||||
- `Claude CLI not found` - Install Claude Code
|
||||
- `Invalid API key` - Check profile settings in `~/.ccs/{profile}.settings.json`
|
||||
|
||||
**Diagnostics:**
|
||||
```bash
|
||||
ccs doctor # Check configuration
|
||||
ccs --version # Show delegation status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
**Entry Point**: `../SKILL.md` - Quick start and decision framework
|
||||
**Decision Guide**: `delegation-guidelines.md` - When to delegate
|
||||
**Error Recovery**: `troubleshooting.md` - Common issues
|
||||
|
||||
**Official Docs**: https://code.claude.com/docs/en/headless.md
|
||||
@@ -0,0 +1,268 @@
|
||||
# Troubleshooting
|
||||
|
||||
AI-oriented error resolution guide for CCS delegation issues.
|
||||
|
||||
## Error Pattern Matching
|
||||
|
||||
### Profile Configuration Errors
|
||||
|
||||
**Pattern:** `Profile 'X' is not configured for delegation`
|
||||
```
|
||||
Root cause: Missing ~/.ccs/{profile}.settings.json
|
||||
Resolution:
|
||||
1. Check file exists: ls ~/.ccs/{profile}.settings.json
|
||||
2. Run diagnostics: ccs doctor
|
||||
3. If missing, user must configure profile manually
|
||||
```
|
||||
|
||||
**Pattern:** `Invalid API key` (401 error)
|
||||
```
|
||||
Root cause: API token expired or invalid
|
||||
Resolution:
|
||||
1. Verify token exists in settings.json
|
||||
2. Test with simple command: ccs {profile} "test"
|
||||
3. If fails, user must regenerate token from provider
|
||||
```
|
||||
|
||||
**Pattern:** `Settings file not found`
|
||||
```
|
||||
Root cause: ~/.ccs/{profile}.settings.json doesn't exist
|
||||
Resolution:
|
||||
1. Run: ccs doctor
|
||||
2. Shows missing profiles
|
||||
3. User must configure manually
|
||||
```
|
||||
|
||||
### Delegation Execution Errors
|
||||
|
||||
**Pattern:** `No previous session found for {profile}`
|
||||
```
|
||||
Root cause: Using :continue without initial session
|
||||
Resolution:
|
||||
- Cannot use ccs {profile}:continue without prior session
|
||||
- Must run: ccs {profile} -p "initial task" first
|
||||
- Then can continue with: ccs {profile}:continue -p "follow-up"
|
||||
Example:
|
||||
[X] ccs glm:continue -p "task" # ERROR: no session
|
||||
[OK] ccs glm -p "task" # Creates session
|
||||
[OK] ccs glm:continue -p "more" # Uses session
|
||||
```
|
||||
|
||||
**Pattern:** `Missing prompt after -p flag`
|
||||
```
|
||||
Root cause: No argument provided after -p
|
||||
Resolution:
|
||||
- Syntax: ccs {profile} -p "prompt text"
|
||||
- Quote prompt if contains spaces
|
||||
Example:
|
||||
[X] ccs glm -p # ERROR
|
||||
[OK] ccs glm -p "add tests" # Correct
|
||||
```
|
||||
|
||||
**Pattern:** `No profile specified`
|
||||
```
|
||||
Root cause: Command missing profile name
|
||||
Resolution:
|
||||
- Syntax: ccs <profile> -p "task"
|
||||
- Available profiles: glm, kimi
|
||||
Example:
|
||||
[X] ccs -p "task" # ERROR: no profile
|
||||
[OK] ccs glm -p "task" # Correct
|
||||
```
|
||||
|
||||
**Pattern:** Exit code 1 with JSON parse error
|
||||
```
|
||||
Root cause: Claude CLI returned non-stream-JSON output
|
||||
Resolution:
|
||||
1. Check if --output-format stream-json is supported
|
||||
2. Verify Claude CLI version (need recent version with stream-json support)
|
||||
3. Test manually: claude -p "test" --output-format stream-json
|
||||
4. If not supported, delegation won't work
|
||||
```
|
||||
|
||||
### Session Management Errors
|
||||
|
||||
**Pattern:** Session file corrupted
|
||||
```
|
||||
Root cause: ~/.ccs/delegation-sessions.json malformed
|
||||
Resolution:
|
||||
1. Backup file: cp ~/.ccs/delegation-sessions.json ~/.ccs/delegation-sessions.json.bak
|
||||
2. Delete corrupted file: rm ~/.ccs/delegation-sessions.json
|
||||
3. New file created on next delegation
|
||||
4. Previous sessions lost but fresh start
|
||||
```
|
||||
|
||||
**Pattern:** Session expired
|
||||
```
|
||||
Root cause: Session older than 30 days
|
||||
Resolution:
|
||||
- Sessions auto-expire after 30 days
|
||||
- Start new session: ccs {profile} -p "task"
|
||||
- Cannot resume expired sessions
|
||||
```
|
||||
|
||||
### Network & API Errors
|
||||
|
||||
**Pattern:** Connection timeout
|
||||
```
|
||||
Root cause: Network issue or API endpoint unreachable
|
||||
Resolution:
|
||||
1. Check internet: ping 8.8.8.8
|
||||
2. Verify API endpoint in settings.json
|
||||
3. Check firewall/proxy settings
|
||||
4. Retry delegation
|
||||
```
|
||||
|
||||
**Pattern:** Rate limiting (429)
|
||||
```
|
||||
Root cause: Too many API requests
|
||||
Resolution:
|
||||
1. Wait 60 seconds before retry
|
||||
2. Reduce concurrent delegations
|
||||
3. Check API quota limits
|
||||
```
|
||||
|
||||
### File Operation Errors
|
||||
|
||||
**Pattern:** File not found during delegation
|
||||
```
|
||||
Root cause: Path doesn't exist or wrong working directory
|
||||
Resolution:
|
||||
1. Delegation runs in cwd where command executed
|
||||
2. Verify file exists: ls <file>
|
||||
3. Use absolute paths in prompt if needed
|
||||
Example:
|
||||
Prompt: "refactor src/auth.js"
|
||||
Check: ls src/auth.js # Must exist in cwd
|
||||
```
|
||||
|
||||
**Pattern:** Permission denied writing files
|
||||
```
|
||||
Root cause: Insufficient permissions in target directory
|
||||
Resolution:
|
||||
1. Check directory permissions: ls -la
|
||||
2. Verify cwd is writable
|
||||
3. Don't delegate in read-only directories
|
||||
```
|
||||
|
||||
## Diagnostic Commands
|
||||
|
||||
**Profile validation:**
|
||||
```bash
|
||||
ccs doctor # Check all profiles
|
||||
cat ~/.ccs/glm.settings.json # Verify settings
|
||||
ccs glm "echo test" 2>&1 # Test execution
|
||||
```
|
||||
|
||||
**Session inspection:**
|
||||
```bash
|
||||
cat ~/.ccs/delegation-sessions.json # View sessions
|
||||
jq '.glm' ~/.ccs/delegation-sessions.json # Check specific profile
|
||||
```
|
||||
|
||||
**Delegation test:**
|
||||
```bash
|
||||
ccs glm -p "create test.txt file with 'hello'" # Simple test
|
||||
cat test.txt # Verify result
|
||||
```
|
||||
|
||||
**Debug mode:**
|
||||
```bash
|
||||
export CCS_DEBUG=1
|
||||
ccs glm -p "task" 2>&1 | tee debug.log # Capture full output
|
||||
```
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```
|
||||
Delegation fails?
|
||||
│
|
||||
├─→ "Profile not configured"
|
||||
│ └─→ Run: ccs doctor
|
||||
│ └─→ Configure missing profile
|
||||
│
|
||||
├─→ "No previous session"
|
||||
│ └─→ Using :continue?
|
||||
│ ├─→ YES: Run initial task first
|
||||
│ └─→ NO: Different error
|
||||
│
|
||||
├─→ "Missing prompt"
|
||||
│ └─→ Check syntax: ccs {profile} -p "prompt"
|
||||
│
|
||||
├─→ Exit code 1
|
||||
│ └─→ Check error message
|
||||
│ ├─→ JSON parse: Claude CLI version issue
|
||||
│ ├─→ File not found: Verify paths
|
||||
│ └─→ API error: Check network/token
|
||||
│
|
||||
└─→ Silent failure
|
||||
└─→ Enable debug: export CCS_DEBUG=1
|
||||
```
|
||||
|
||||
## Common Patterns to Avoid
|
||||
|
||||
**Anti-pattern:** Delegating without profile validation
|
||||
```
|
||||
[X] Assume profile exists
|
||||
[OK] Run ccs doctor first to verify
|
||||
```
|
||||
|
||||
**Anti-pattern:** Using :continue immediately
|
||||
```
|
||||
[X] ccs glm:continue -p "task" # No initial session
|
||||
[OK] ccs glm -p "task" && ccs glm:continue -p "more"
|
||||
```
|
||||
|
||||
**Anti-pattern:** Delegating complex tasks
|
||||
```
|
||||
[X] ccs glm -p "implement OAuth2" # Too complex
|
||||
[OK] ccs glm -p "add tests for login function"
|
||||
```
|
||||
|
||||
**Anti-pattern:** Vague prompts
|
||||
```
|
||||
[X] ccs glm -p "fix the bug" # No context
|
||||
[OK] ccs glm -p "fix typo in src/auth.js line 42"
|
||||
```
|
||||
|
||||
## Recovery Procedures
|
||||
|
||||
**Reset session state:**
|
||||
```bash
|
||||
rm ~/.ccs/delegation-sessions.json
|
||||
# Fresh start, all sessions lost
|
||||
```
|
||||
|
||||
**Reconfigure profile:**
|
||||
```bash
|
||||
ccs doctor # Shows issues
|
||||
# Edit ~/.ccs/{profile}.settings.json manually
|
||||
# Verify: ccs {profile} "test"
|
||||
```
|
||||
|
||||
**Test delegation flow:**
|
||||
```bash
|
||||
# 1. Simple task
|
||||
ccs glm -p "create test.txt with content 'hello'"
|
||||
|
||||
# 2. Verify session created
|
||||
cat ~/.ccs/delegation-sessions.json | jq '.glm.sessionId'
|
||||
|
||||
# 3. Test continue
|
||||
ccs glm:continue -p "append 'world' to test.txt"
|
||||
|
||||
# 4. Verify aggregation
|
||||
cat ~/.ccs/delegation-sessions.json | jq '.glm.turns'
|
||||
```
|
||||
|
||||
## Emergency Fallback
|
||||
|
||||
If delegation completely broken:
|
||||
```bash
|
||||
# Use Claude CLI directly
|
||||
claude -p "task" --settings ~/.ccs/glm.settings.json
|
||||
|
||||
# Bypass delegation (no -p flag)
|
||||
ccs glm
|
||||
# Then work interactively
|
||||
```
|
||||
@@ -2,6 +2,49 @@
|
||||
|
||||
Format: [Keep a Changelog](https://keepachangelog.com/)
|
||||
|
||||
## [4.1.0] - 2025-11-16
|
||||
|
||||
### Added
|
||||
- **Selective .claude/ directory symlinking** for shared resources across profiles
|
||||
- `claude-symlink-manager.js` utility for managing symlinks with Windows fallback
|
||||
- Enhanced `ccs doctor` command to verify .claude/ directory health
|
||||
- Postinstall script for automatic .claude/ directory setup
|
||||
- **Stream-JSON output** for real-time delegation visibility (`--output-format stream-json --verbose`)
|
||||
- **Real-time tool tracking** with verbose context (shows file paths, commands, patterns)
|
||||
- **Smart slash command detection** (preserves /cook, /plan, /commit in delegated prompts)
|
||||
- **Signal handling** (Ctrl+C/Esc kills delegated child processes, prevents orphans)
|
||||
- **Comprehensive tool support** (13 Claude Code tools: Bash, Read, Write, Edit, Glob, Grep, NotebookEdit, NotebookRead, SlashCommand, Task, TodoWrite, WebFetch, WebSearch)
|
||||
- **Active task display** for TodoWrite (shows current task instead of count)
|
||||
- Documentation: Stream-JSON workflow diagrams
|
||||
|
||||
### Changed
|
||||
- Installers now create selective symlinks (commands/, skills/, agents/) instead of full directory copies
|
||||
- Windows support: Falls back to directory copying when symlinks unavailable
|
||||
- Profile-specific files (settings.json, sessions/, todolists/, logs/) remain isolated
|
||||
- Improved README with symlink architecture documentation
|
||||
- **BREAKING**: Delegation now uses stream-json instead of single JSON blob
|
||||
- **Time-based limits** replace turn-based limits (10min default timeout vs 20 max-turns)
|
||||
- **Graceful termination** with SIGTERM → SIGKILL fallback (2s grace period)
|
||||
- Removed `--max-turns` flag (deprecated, use timeout instead)
|
||||
- Simplified slash command docs (removed over-prescriptive instructions)
|
||||
- Internal tools (TodoWrite, Skill) now show meaningful progress
|
||||
|
||||
### Fixed
|
||||
- Duplicate .claude/ resources across multiple profiles
|
||||
- Installer logic now handles symlink creation during setup
|
||||
- Orphaned `claude -p` processes after parent termination
|
||||
- Slash commands broken by IMPORTANT safety prefix
|
||||
- Slash commands detected as file paths (/home vs /cook)
|
||||
- Stream-json requires `--verbose` flag with `-p`
|
||||
- Tool output spam (filtered internal tools, show active tasks)
|
||||
|
||||
### Removed
|
||||
- IMPORTANT safety prefix (broke slash command positioning)
|
||||
- Outdated test files (json-output.test.js, max-turns.test.js)
|
||||
- TTY detection (now shows progress unless CCS_QUIET=1)
|
||||
|
||||
---
|
||||
|
||||
## [3.5.0] - 2025-11-15
|
||||
|
||||
### Added
|
||||
|
||||
@@ -174,29 +174,51 @@ Then: 🔴 _"You've reached your usage limit."_
|
||||
<details>
|
||||
<summary><strong>❌ OLD WAY:</strong> Switch When You Hit Limits (Reactive)</summary>
|
||||
|
||||
### Your Current Workflow:
|
||||
- **2pm:** Building features, in the zone
|
||||
- **3pm:** 🔴 Usage limit hit
|
||||
- **3:05pm:** Stop work, edit `~/.claude/settings.json`
|
||||
- **3:15pm:** Switch accounts, lose context
|
||||
- **3:30pm:** Try to get back in flow state
|
||||
- **4pm:** Finally productive again
|
||||
<br>
|
||||
|
||||
- **Result:** 1 hour lost, momentum destroyed, frustration builds
|
||||
```mermaid
|
||||
graph LR
|
||||
A[2pm: Building features<br/>In the zone] --> B[3pm: Usage limit hit<br/>BLOCKED]
|
||||
B --> C[3:05pm: Stop work<br/>Edit settings.json]
|
||||
C --> D[3:15pm: Switch accounts<br/>Context lost]
|
||||
D --> E[3:30pm: Restart<br/>Trying to focus]
|
||||
E --> F[4pm: Finally productive<br/>Back in flow]
|
||||
|
||||
style A fill:#d4edda,stroke:#333,color:#000
|
||||
style B fill:#f8d7da,stroke:#333,color:#000
|
||||
style C fill:#fff3cd,stroke:#333,color:#000
|
||||
style D fill:#f8d7da,stroke:#333,color:#000
|
||||
style E fill:#fff3cd,stroke:#333,color:#000
|
||||
style F fill:#d4edda,stroke:#333,color:#000
|
||||
```
|
||||
|
||||
**Result:** 1 hour lost, momentum destroyed, frustration builds
|
||||
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>✨ NEW WAY:</strong> Run Parallel From Start (Proactive) - <strong>RECOMMENDED</strong></summary>
|
||||
|
||||
### Your New Workflow:
|
||||
- **2pm:** **Terminal 1:** `ccs "Plan the API architecture"` → Strategic thinking (Claude Pro)
|
||||
- **2pm:** **Terminal 2:** `ccs glm "Implement the endpoints"` → Code execution (GLM)
|
||||
- **3pm:** Still shipping, no interruptions
|
||||
- **4pm:** Flow state achieved, productivity spiking
|
||||
- **5pm:** Features shipped, context maintained
|
||||
<br>
|
||||
|
||||
- **Result:** Zero downtime, continuous productivity, less frustration
|
||||
```mermaid
|
||||
graph LR
|
||||
A[2pm: Start work] --> B[Terminal 1: Claude Pro<br/>Strategic planning]
|
||||
A --> C[Terminal 2: GLM<br/>Code execution]
|
||||
B --> D[3pm: Still shipping<br/>No interruptions]
|
||||
C --> D
|
||||
D --> E[4pm: Flow state<br/>Productivity peak]
|
||||
E --> F[5pm: Features shipped<br/>Context maintained]
|
||||
|
||||
style A fill:#e7f3ff,stroke:#333,color:#000
|
||||
style B fill:#cfe2ff,stroke:#333,color:#000
|
||||
style C fill:#cfe2ff,stroke:#333,color:#000
|
||||
style D fill:#d4edda,stroke:#333,color:#000
|
||||
style E fill:#d4edda,stroke:#333,color:#000
|
||||
style F fill:#d4edda,stroke:#333,color:#000
|
||||
```
|
||||
|
||||
**Result:** Zero downtime, continuous productivity, less frustration
|
||||
|
||||
### 💰 **The Value Proposition:**
|
||||
- **Setup:** Your existing Claude Pro + GLM Lite (cost-effective add-on)
|
||||
@@ -291,16 +313,22 @@ Then: 🔴 _"You've reached your usage limit."_
|
||||
- Uses `CLAUDE_CONFIG_DIR` for isolated instances
|
||||
- Create with `ccs auth create <profile>`
|
||||
|
||||
### Shared Data (v3.1)
|
||||
### Shared Data (v3.1+)
|
||||
|
||||
Commands and skills symlinked from `~/.ccs/shared/` - **no duplication across profiles**.
|
||||
**CCS items (v4.1)**: Commands and skills symlinked from `~/.ccs/.claude/` to `~/.claude/` - **single source of truth with auto-propagation**.
|
||||
|
||||
**Profile access**: `~/.ccs/shared/` symlinks to `~/.claude/` - **no duplication across profiles**.
|
||||
|
||||
```plaintext
|
||||
~/.ccs/
|
||||
├── shared/ # Shared across all profiles
|
||||
│ ├── agents/
|
||||
│ ├── commands/
|
||||
│ └── skills/
|
||||
├── .claude/ # CCS items (ships with package, v4.1)
|
||||
│ ├── commands/ccs/ # Delegation commands (/ccs:glm, /ccs:kimi)
|
||||
│ ├── skills/ccs-delegation/ # AI decision framework
|
||||
│ └── agents/ccs-delegator.md # Proactive delegation agent
|
||||
├── shared/ # Symlinks to ~/.claude/ (for profiles)
|
||||
│ ├── agents@ → ~/.claude/agents/
|
||||
│ ├── commands@ → ~/.claude/commands/
|
||||
│ └── skills@ → ~/.claude/skills/
|
||||
├── instances/ # Profile-specific data
|
||||
│ └── work/
|
||||
│ ├── agents@ → shared/agents/
|
||||
@@ -309,15 +337,23 @@ Commands and skills symlinked from `~/.ccs/shared/` - **no duplication across pr
|
||||
│ ├── settings.json # API keys, credentials
|
||||
│ ├── sessions/ # Conversation history
|
||||
│ └── ...
|
||||
|
||||
~/.claude/ # User's Claude directory
|
||||
├── commands/ccs@ → ~/.ccs/.claude/commands/ccs/ # Selective symlink
|
||||
├── skills/ccs-delegation@ → ~/.ccs/.claude/skills/ccs-delegation/
|
||||
└── agents/ccs-delegator.md@ → ~/.ccs/.claude/agents/ccs-delegator.md
|
||||
```
|
||||
|
||||
**Symlink Chain**: `work profile → ~/.ccs/shared/ → ~/.claude/ → ~/.ccs/.claude/` (CCS items)
|
||||
|
||||
| Type | Files |
|
||||
|:-----|:------|
|
||||
| **Shared** | `commands/`, `skills/`, `agents/` |
|
||||
| **CCS items** | `~/.ccs/.claude/` (ships with package, selective symlinks to `~/.claude/`) |
|
||||
| **Shared** | `~/.ccs/shared/` (symlinks to `~/.claude/`) |
|
||||
| **Profile-specific** | `settings.json`, `sessions/`, `todolists/`, `logs/` |
|
||||
|
||||
> [!NOTE]
|
||||
> **Windows**: Copies directories if symlinks unavailable (enable Developer Mode for true symlinks)
|
||||
> **Windows**: Symlink support requires Developer Mode (v4.2 will add copy fallback)
|
||||
|
||||
<br>
|
||||
|
||||
@@ -358,6 +394,123 @@ ccs --help # Show all commands and options
|
||||
|
||||
<br>
|
||||
|
||||
## AI-Powered Delegation
|
||||
|
||||
> [!TIP]
|
||||
> **New in v4.0**: Delegate tasks to cost-optimized models (GLM, Kimi) directly from your main Claude session. Save 81% on simple tasks with real-time visibility.
|
||||
|
||||
### What is Delegation?
|
||||
|
||||
CCS Delegation lets you **send tasks to alternative models** (`glm`, `kimi`) **from your main Claude session** using the `-p` flag or slash commands (`/ccs:glm`, `/ccs:kimi`).
|
||||
|
||||
**Why use it?**
|
||||
- **Token efficiency**: Simple tasks cost 81% less on GLM vs main Claude session
|
||||
- **Context preservation**: Main session stays clean, no pollution from mechanical tasks
|
||||
- **Real-time visibility**: See tool usage as tasks execute (`[Tool] Write: index.html`)
|
||||
- **Multi-turn support**: Resume sessions with `:continue` for iterative work
|
||||
|
||||
### Quick Examples
|
||||
|
||||
**Direct CLI:**
|
||||
```bash
|
||||
# Delegate simple task to GLM (cost-optimized)
|
||||
ccs glm -p "add tests for UserService"
|
||||
|
||||
# Delegate long-context task to Kimi
|
||||
ccs kimi -p "analyze all files in src/ and document architecture"
|
||||
|
||||
# Continue previous session
|
||||
ccs glm:continue -p "run the tests and fix any failures"
|
||||
```
|
||||
|
||||
**Via Slash Commands** (inside Claude sessions):
|
||||
```bash
|
||||
# In your main Claude session:
|
||||
/ccs:glm "refactor auth.js to use async/await"
|
||||
/ccs:kimi "find all deprecated API usages across codebase"
|
||||
/ccs:glm:continue "also update the README examples"
|
||||
```
|
||||
|
||||
**Via Natural Language** (Claude auto-delegates):
|
||||
```bash
|
||||
# Claude detects delegation patterns and auto-executes:
|
||||
"Use ccs glm to add tests for all *.service.js files"
|
||||
"Delegate to kimi: analyze project structure"
|
||||
```
|
||||
|
||||
### Real-Time Output
|
||||
|
||||
See exactly what's happening as tasks execute:
|
||||
|
||||
```
|
||||
$ ccs glm -p "/cook create a landing page"
|
||||
[i] Delegating to GLM-4.6...
|
||||
[Tool] Write: /home/user/project/index.html
|
||||
[Tool] Write: /home/user/project/styles.css
|
||||
[Tool] Write: /home/user/project/script.js
|
||||
[Tool] Edit: /home/user/project/styles.css
|
||||
[i] Execution completed in 45.2s
|
||||
|
||||
╔══════════════════════════════════════════════════════╗
|
||||
║ Working Directory: /home/user/project ║
|
||||
║ Model: GLM-4.6 ║
|
||||
║ Duration: 45.2s ║
|
||||
║ Exit Code: 0 ║
|
||||
║ Session ID: 3a4f8c21 ║
|
||||
║ Total Cost: $0.0015 ║
|
||||
║ Turns: 3 ║
|
||||
╚══════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
### Advanced Features
|
||||
|
||||
**Slash Command Support:**
|
||||
Delegation preserves custom slash commands in prompts:
|
||||
```bash
|
||||
ccs glm -p "/cook create responsive landing page"
|
||||
# Executes /cook command in delegated GLM session
|
||||
```
|
||||
|
||||
**Signal Handling:**
|
||||
Ctrl+C or Esc properly kills delegated processes (no orphans):
|
||||
```bash
|
||||
# Hit Ctrl+C during delegation
|
||||
[!] Parent process terminating, killing delegated session...
|
||||
```
|
||||
|
||||
**Time-Based Limits:**
|
||||
10-minute default timeout with graceful termination (supports `:continue`):
|
||||
```bash
|
||||
ccs glm -p "complex task" # Auto-terminates after 10min if needed
|
||||
ccs glm:continue -p "pick up where we left off"
|
||||
```
|
||||
|
||||
### Cost Savings Example
|
||||
|
||||
**Traditional (Main Session):**
|
||||
```
|
||||
Context load: 2000 tokens
|
||||
Discussion: 1500 tokens
|
||||
Code gen: 4500 tokens
|
||||
─────────────────────────
|
||||
Total: 8000 tokens → $0.032
|
||||
```
|
||||
|
||||
**Delegation (GLM):**
|
||||
```
|
||||
3x tasks via GLM: 1500 tokens → $0.0045
|
||||
─────────────────────────────────────────
|
||||
Savings: $0.0275 (86% reduction)
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
- **Workflow Diagrams**: See [docs/ccs-delegation-diagrams.md](docs/ccs-delegation-diagrams.md) for visual architecture
|
||||
- **Skill Reference**: `.claude/skills/ccs-delegation/` for AI decision framework
|
||||
- **Agent Docs**: `.claude/agents/ccs-delegator.md` for orchestration patterns
|
||||
|
||||
<br>
|
||||
|
||||
## GLM with Thinking (GLMT)
|
||||
|
||||
> [!CAUTION]
|
||||
@@ -568,6 +721,53 @@ cat ~/.ccs/logs/*response-openai.json | jq '.choices[0].message.reasoning_conten
|
||||
|
||||
<br>
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Health Check
|
||||
|
||||
Run diagnostics to verify your CCS installation:
|
||||
|
||||
```bash
|
||||
ccs doctor
|
||||
```
|
||||
|
||||
**Checks performed**:
|
||||
- ✓ Claude CLI availability
|
||||
- ✓ Configuration files (config.json, profiles)
|
||||
- ✓ CCS symlinks to ~/.claude/
|
||||
- ✓ Delegation system
|
||||
- ✓ File permissions
|
||||
|
||||
**Output**:
|
||||
```
|
||||
[?] Checking Claude CLI... [OK]
|
||||
[?] Checking ~/.ccs/ directory... [OK]
|
||||
[?] Checking config.json... [OK]
|
||||
[?] Checking CCS symlinks... [OK]
|
||||
...
|
||||
Status: Installation healthy
|
||||
```
|
||||
|
||||
### Update CCS Items
|
||||
|
||||
If you modify CCS items or need to re-install symlinks:
|
||||
|
||||
```bash
|
||||
ccs update
|
||||
```
|
||||
|
||||
**What it does**:
|
||||
- Re-creates selective symlinks from `~/.ccs/.claude/` to `~/.claude/`
|
||||
- Backs up existing files before replacing
|
||||
- Safe to run multiple times (idempotent)
|
||||
|
||||
**When to use**:
|
||||
- After manual modifications to ~/.claude/
|
||||
- If `ccs doctor` reports symlink issues
|
||||
- After upgrading CCS to a new version
|
||||
|
||||
<br>
|
||||
|
||||
## Uninstall
|
||||
|
||||
<details>
|
||||
|
||||
+61
@@ -73,6 +73,33 @@ function handleVersionCommand() {
|
||||
// Config path
|
||||
const configPath = getConfigPath();
|
||||
console.log(` ${colored('Config:', 'cyan')} ${configPath}`);
|
||||
|
||||
// Delegation status
|
||||
const delegationRulesPath = path.join(os.homedir(), '.ccs', 'delegation-rules.json');
|
||||
const delegationEnabled = fs.existsSync(delegationRulesPath);
|
||||
|
||||
if (delegationEnabled) {
|
||||
console.log(` ${colored('Delegation:', 'cyan')} Enabled`);
|
||||
|
||||
// Check which profiles are delegation-ready
|
||||
const readyProfiles = [];
|
||||
const { DelegationValidator } = require('./utils/delegation-validator');
|
||||
|
||||
for (const profile of ['glm', 'kimi']) {
|
||||
const validation = DelegationValidator.validate(profile);
|
||||
if (validation.valid) {
|
||||
readyProfiles.push(profile);
|
||||
}
|
||||
}
|
||||
|
||||
if (readyProfiles.length > 0) {
|
||||
console.log(` ${colored('Ready:', 'cyan')} ${readyProfiles.join(', ')}`);
|
||||
} else {
|
||||
console.log(` ${colored('Ready:', 'cyan')} None (configure profiles first)`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ${colored('Delegation:', 'cyan')} Not configured`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Documentation
|
||||
@@ -121,9 +148,19 @@ function handleHelpCommand() {
|
||||
console.log(` ${colored('ccs personal', 'yellow')} Switch to personal account`);
|
||||
console.log('');
|
||||
|
||||
// Delegation (NEW)
|
||||
console.log(colored('Delegation (Token Optimization):', 'cyan'));
|
||||
console.log(` ${colored('/ccs:glm "task"', 'yellow')} Delegate to GLM-4.6 within Claude session`);
|
||||
console.log(` ${colored('/ccs:kimi "task"', 'yellow')} Delegate to Kimi for long context`);
|
||||
console.log(` ${colored('/ccs:create m2', 'yellow')} Create custom delegation command`);
|
||||
console.log(' Use delegation to save tokens on simple tasks');
|
||||
console.log(' Commands work inside Claude Code sessions only');
|
||||
console.log('');
|
||||
|
||||
// Diagnostics
|
||||
console.log(colored('Diagnostics:', 'cyan'));
|
||||
console.log(` ${colored('ccs doctor', 'yellow')} Run health check and diagnostics`);
|
||||
console.log(` ${colored('ccs update', 'yellow')} Re-install CCS items to ~/.claude/`);
|
||||
console.log('');
|
||||
|
||||
// Flags
|
||||
@@ -217,6 +254,16 @@ async function handleDoctorCommand() {
|
||||
process.exit(doctor.results.isHealthy() ? 0 : 1);
|
||||
}
|
||||
|
||||
async function handleUpdateCommand() {
|
||||
const ClaudeSymlinkManager = require('./utils/claude-symlink-manager');
|
||||
const manager = new ClaudeSymlinkManager();
|
||||
|
||||
console.log('[i] Updating CCS items in ~/.claude/...');
|
||||
manager.update();
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Smart profile detection
|
||||
function detectProfile(args) {
|
||||
if (args.length === 0 || args[0].startsWith('-')) {
|
||||
@@ -454,6 +501,12 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: update command (re-install CCS symlinks)
|
||||
if (firstArg === 'update' || firstArg === '--update') {
|
||||
await handleUpdateCommand();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: auth command (multi-account management)
|
||||
if (firstArg === 'auth') {
|
||||
const AuthCommands = require('./auth/auth-commands');
|
||||
@@ -462,6 +515,14 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: headless delegation (-p flag)
|
||||
if (args.includes('-p') || args.includes('--prompt')) {
|
||||
const DelegationHandler = require('./delegation/delegation-handler');
|
||||
const handler = new DelegationHandler();
|
||||
await handler.route(args);
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-recovery for missing configuration
|
||||
const recovery = new RecoveryManager();
|
||||
const recovered = recovery.recoverAll();
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# CCS Delegation Module
|
||||
|
||||
Enhanced Claude Code delegation system for multi-model task delegation.
|
||||
|
||||
## Files
|
||||
|
||||
### Core Components
|
||||
- **headless-executor.js** (405 lines) - Main executor, spawns `claude -p` with enhanced features
|
||||
- **session-manager.js** (156 lines) - Session persistence and cost tracking
|
||||
- **settings-parser.js** (88 lines) - Parse tool restrictions from settings
|
||||
- **result-formatter.js** (326 lines) - Terminal output formatting
|
||||
|
||||
**Total**: 975 lines (down from 1,755 lines - 44% reduction)
|
||||
|
||||
## Features
|
||||
|
||||
### Enhanced Headless Execution
|
||||
- Stream-JSON output parsing (`--output-format stream-json`)
|
||||
- Real-time tool use visibility in TTY
|
||||
- Permission mode acceptEdits (`--permission-mode acceptEdits`)
|
||||
- Tool restrictions from `.claude/settings.local.json`
|
||||
- Multi-turn session management (`--resume <session-id>`)
|
||||
- Time-based limits (10 min default timeout with graceful termination)
|
||||
- Cost tracking and aggregation
|
||||
|
||||
### Session Management
|
||||
- Persistence: `~/.ccs/delegation-sessions.json`
|
||||
- Resume via `/ccs:glm:continue` and `/ccs:kimi:continue`
|
||||
- Auto-cleanup expired sessions (>30 days)
|
||||
- Cost aggregation across turns
|
||||
|
||||
### Settings
|
||||
- Profile location: `~/.ccs/{profile}.settings.json`
|
||||
- Examples: `glm.settings.json`, `kimi.settings.json`, `glmt.settings.json`
|
||||
- Tool restrictions from `.claude/settings.local.json`
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Delegation
|
||||
```javascript
|
||||
const { HeadlessExecutor } = require('./headless-executor');
|
||||
|
||||
const result = await HeadlessExecutor.execute('glm', 'Refactor auth.js', {
|
||||
cwd: '/path/to/project',
|
||||
outputFormat: 'stream-json',
|
||||
permissionMode: 'acceptEdits',
|
||||
timeout: 600000 // 10 minutes
|
||||
});
|
||||
|
||||
console.log(result.sessionId); // For multi-turn
|
||||
console.log(result.totalCost); // Cost in USD
|
||||
console.log(result.content); // Result text
|
||||
```
|
||||
|
||||
### Multi-Turn Sessions
|
||||
```javascript
|
||||
// Start session
|
||||
const result1 = await HeadlessExecutor.execute('glm', 'Implement feature');
|
||||
const sessionId = result1.sessionId;
|
||||
|
||||
// Continue session
|
||||
const result2 = await HeadlessExecutor.execute('glm', 'Add tests', {
|
||||
resumeSession: true
|
||||
});
|
||||
|
||||
// Or with specific session ID
|
||||
const result3 = await HeadlessExecutor.execute('glm', 'Run tests', {
|
||||
sessionId: sessionId
|
||||
});
|
||||
```
|
||||
|
||||
### Tool Restrictions
|
||||
Create `.claude/settings.local.json`:
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"allow": ["Bash(git:*)", "Read", "Edit"],
|
||||
"deny": ["Bash(rm:*)", "Bash(sudo:*)"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Automatically applied as CLI flags:
|
||||
```bash
|
||||
--allowedTools "Bash(git:*)" "Read" "Edit" \
|
||||
--disallowedTools "Bash(rm:*)" "Bash(sudo:*)"
|
||||
```
|
||||
|
||||
## Slash Commands
|
||||
|
||||
The delegation system is invoked via simple slash commands in `.claude/commands/ccs/`:
|
||||
|
||||
### Basic Commands
|
||||
- `/ccs:glm "task"` - Delegate to GLM-4.6
|
||||
- `/ccs:kimi "task"` - Delegate to Kimi (long-context)
|
||||
|
||||
### Multi-Turn Commands
|
||||
- `/ccs:glm:continue "follow-up"` - Resume last GLM session
|
||||
- `/ccs:kimi:continue "follow-up"` - Resume last Kimi session
|
||||
|
||||
Each command directly invokes:
|
||||
```bash
|
||||
claude -p "$ARGUMENTS" \
|
||||
--settings ~/.ccs/{profile}.settings.json \
|
||||
--output-format stream-json \
|
||||
--permission-mode acceptEdits
|
||||
```
|
||||
|
||||
## Debug Mode
|
||||
|
||||
```bash
|
||||
export CCS_DEBUG=1
|
||||
```
|
||||
|
||||
Enables verbose logging:
|
||||
- Permission mode selection
|
||||
- Session resumption details
|
||||
- Tool restrictions parsing
|
||||
- CLI args construction
|
||||
- Session persistence events
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all delegation tests
|
||||
node tests/unit/delegation/json-output.test.js
|
||||
node tests/unit/delegation/permission-mode.test.js
|
||||
node tests/unit/delegation/session-manager.test.js
|
||||
node tests/unit/delegation/settings-parser.test.js
|
||||
node tests/unit/delegation/max-turns.test.js
|
||||
node tests/unit/delegation/result-formatter.test.js
|
||||
```
|
||||
|
||||
**Test Coverage:**
|
||||
- JSON output parsing (6 tests)
|
||||
- Permission modes (11 tests)
|
||||
- Session management (7 tests)
|
||||
- Settings parser (6 tests)
|
||||
- Auto max-turns (14 tests)
|
||||
- Result formatting (14 tests)
|
||||
- **Total: 58 tests**
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
User → SlashCommand (/ccs:glm)
|
||||
→ Directly invokes: claude -p
|
||||
→ HeadlessExecutor (monitors execution)
|
||||
→ SessionManager (load last session)
|
||||
→ SettingsParser (tool restrictions)
|
||||
→ Parse JSON response
|
||||
→ SessionManager (store/update)
|
||||
→ ResultFormatter.format()
|
||||
→ Display to user
|
||||
```
|
||||
|
||||
**Key Simplification**: Slash commands invoke `claude -p` directly. No intermediate delegation engine or rule system - just direct headless execution with enhanced features.
|
||||
|
||||
## File Permissions
|
||||
|
||||
All files should be `644` (rw-r--r--):
|
||||
```bash
|
||||
chmod 644 bin/delegation/*.js
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Node.js 14+
|
||||
- Claude CLI installed and in PATH
|
||||
- Profile settings configured in `~/.ccs/{profile}.settings.json`
|
||||
|
||||
## Migration from Legacy System
|
||||
|
||||
**Removed components** (as of 2025-11-15):
|
||||
- ~~delegation-engine.js~~ - Rule-based decision engine (unused)
|
||||
- ~~cwd-resolver.js~~ - Working directory resolution (unused)
|
||||
- ~~rules-schema.js~~ - Schema validation (unused)
|
||||
- ~~delegation-rules.json~~ - Configuration file (not created)
|
||||
|
||||
**Why removed**: Current slash commands directly invoke `claude -p` without intermediate orchestration. The delegation engine, CWD resolver, and rules schema were designed for a more complex system that was never fully integrated.
|
||||
|
||||
**Result**: 44% code reduction (1,755 → 975 lines) with same functionality.
|
||||
|
||||
## References
|
||||
|
||||
- Official docs: https://code.claude.com/docs/en/headless.md
|
||||
- Skill: `.claude/skills/ccs-delegation/`
|
||||
- Commands: `.claude/commands/ccs/`
|
||||
- Tests: `tests/unit/delegation/`
|
||||
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const { HeadlessExecutor } = require('./headless-executor');
|
||||
const { SessionManager } = require('./session-manager');
|
||||
const { ResultFormatter } = require('./result-formatter');
|
||||
const { DelegationValidator } = require('../utils/delegation-validator');
|
||||
const { SettingsParser } = require('./settings-parser');
|
||||
|
||||
/**
|
||||
* Delegation command handler
|
||||
* Routes -p flag commands to HeadlessExecutor with enhanced features
|
||||
*/
|
||||
class DelegationHandler {
|
||||
/**
|
||||
* Route delegation command
|
||||
* @param {Array<string>} args - Full args array from ccs.js
|
||||
*/
|
||||
async route(args) {
|
||||
try {
|
||||
// 1. Parse args into { profile, prompt, options }
|
||||
const parsed = this._parseArgs(args);
|
||||
|
||||
// 2. Detect special profiles (glm:continue, kimi:continue)
|
||||
if (parsed.profile.includes(':continue')) {
|
||||
return await this._handleContinue(parsed);
|
||||
}
|
||||
|
||||
// 3. Validate profile
|
||||
this._validateProfile(parsed.profile);
|
||||
|
||||
// 4. Execute via HeadlessExecutor
|
||||
const result = await HeadlessExecutor.execute(
|
||||
parsed.profile,
|
||||
parsed.prompt,
|
||||
parsed.options
|
||||
);
|
||||
|
||||
// 5. Format and display results
|
||||
const formatted = ResultFormatter.format(result);
|
||||
console.log(formatted);
|
||||
|
||||
// 6. Exit with proper code
|
||||
process.exit(result.exitCode || 0);
|
||||
} catch (error) {
|
||||
console.error(`[X] Delegation error: ${error.message}`);
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle continue command (resume last session)
|
||||
* @param {Object} parsed - Parsed args
|
||||
*/
|
||||
async _handleContinue(parsed) {
|
||||
const baseProfile = parsed.profile.replace(':continue', '');
|
||||
|
||||
// Get last session from SessionManager
|
||||
const sessionMgr = new SessionManager();
|
||||
const lastSession = sessionMgr.getLastSession(baseProfile);
|
||||
|
||||
if (!lastSession) {
|
||||
console.error(`[X] No previous session found for ${baseProfile}`);
|
||||
console.error(` Start a new session first with: ccs ${baseProfile} -p "task"`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Execute with resume flag
|
||||
const result = await HeadlessExecutor.execute(
|
||||
baseProfile,
|
||||
parsed.prompt,
|
||||
{
|
||||
...parsed.options,
|
||||
resumeSession: true,
|
||||
sessionId: lastSession.sessionId
|
||||
}
|
||||
);
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
console.log(formatted);
|
||||
|
||||
process.exit(result.exitCode || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse args into structured format
|
||||
* @param {Array<string>} args - Raw args
|
||||
* @returns {Object} { profile, prompt, options }
|
||||
*/
|
||||
_parseArgs(args) {
|
||||
// Extract profile (first non-flag arg or 'default')
|
||||
const profile = this._extractProfile(args);
|
||||
|
||||
// Extract prompt from -p or --prompt
|
||||
const prompt = this._extractPrompt(args);
|
||||
|
||||
// Extract options (--timeout, --permission-mode, etc.)
|
||||
const options = this._extractOptions(args);
|
||||
|
||||
return { profile, prompt, options };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract profile from args (first non-flag arg)
|
||||
* @param {Array<string>} args - Args array
|
||||
* @returns {string} Profile name
|
||||
*/
|
||||
_extractProfile(args) {
|
||||
// Find first arg that doesn't start with '-' and isn't -p value
|
||||
let skipNext = false;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (skipNext) {
|
||||
skipNext = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (args[i] === '-p' || args[i] === '--prompt') {
|
||||
skipNext = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!args[i].startsWith('-')) {
|
||||
return args[i];
|
||||
}
|
||||
}
|
||||
|
||||
// No profile specified, return null (will error in validation)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract prompt from -p flag
|
||||
* @param {Array<string>} args - Args array
|
||||
* @returns {string} Prompt text
|
||||
*/
|
||||
_extractPrompt(args) {
|
||||
const pIndex = args.indexOf('-p');
|
||||
const promptIndex = args.indexOf('--prompt');
|
||||
|
||||
const index = pIndex !== -1 ? pIndex : promptIndex;
|
||||
|
||||
if (index === -1 || index === args.length - 1) {
|
||||
console.error('[X] Missing prompt after -p flag');
|
||||
console.error(' Usage: ccs glm -p "task description"');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return args[index + 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract options from remaining args
|
||||
* @param {Array<string>} args - Args array
|
||||
* @returns {Object} Options for HeadlessExecutor
|
||||
*/
|
||||
_extractOptions(args) {
|
||||
const cwd = process.cwd();
|
||||
|
||||
// Read default permission mode from .claude/settings.local.json
|
||||
// Falls back to 'acceptEdits' if file doesn't exist
|
||||
const defaultPermissionMode = SettingsParser.parseDefaultPermissionMode(cwd);
|
||||
|
||||
const options = {
|
||||
cwd,
|
||||
outputFormat: 'stream-json',
|
||||
permissionMode: defaultPermissionMode
|
||||
};
|
||||
|
||||
// Parse permission-mode (CLI flag overrides settings file)
|
||||
const permModeIndex = args.indexOf('--permission-mode');
|
||||
if (permModeIndex !== -1 && permModeIndex < args.length - 1) {
|
||||
options.permissionMode = args[permModeIndex + 1];
|
||||
}
|
||||
|
||||
// Parse timeout
|
||||
const timeoutIndex = args.indexOf('--timeout');
|
||||
if (timeoutIndex !== -1 && timeoutIndex < args.length - 1) {
|
||||
options.timeout = parseInt(args[timeoutIndex + 1], 10);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate profile exists and is configured
|
||||
* @param {string} profile - Profile name
|
||||
*/
|
||||
_validateProfile(profile) {
|
||||
if (!profile) {
|
||||
console.error('[X] No profile specified');
|
||||
console.error(' Usage: ccs <profile> -p "task"');
|
||||
console.error(' Examples: ccs glm -p "task", ccs kimi -p "task"');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Use DelegationValidator to check profile
|
||||
const validation = DelegationValidator.validate(profile);
|
||||
if (!validation.valid) {
|
||||
console.error(`[X] Profile '${profile}' is not configured for delegation`);
|
||||
console.error(` ${validation.error}`);
|
||||
console.error('');
|
||||
console.error(' Run: ccs doctor');
|
||||
console.error(' Or configure: ~/.ccs/${profile}.settings.json');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DelegationHandler;
|
||||
@@ -0,0 +1,617 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const { SessionManager } = require('./session-manager');
|
||||
const { SettingsParser } = require('./settings-parser');
|
||||
|
||||
/**
|
||||
* Headless executor for Claude CLI delegation
|
||||
* Spawns claude with -p flag for single-turn execution
|
||||
*/
|
||||
class HeadlessExecutor {
|
||||
/**
|
||||
* Execute task via headless Claude CLI
|
||||
* @param {string} profile - Profile name (glm, kimi, custom)
|
||||
* @param {string} enhancedPrompt - Enhanced prompt with context
|
||||
* @param {Object} options - Execution options
|
||||
* @param {string} options.cwd - Working directory (absolute path)
|
||||
* @param {number} options.timeout - Timeout in milliseconds (default: 600000 = 10 minutes)
|
||||
* @param {string} options.outputFormat - Output format: 'stream-json' or 'text' (default: 'stream-json')
|
||||
* @param {string} options.permissionMode - Permission mode: 'default', 'plan', 'acceptEdits', 'bypassPermissions' (default: 'acceptEdits')
|
||||
* @param {boolean} options.resumeSession - Resume last session for profile (default: false)
|
||||
* @param {string} options.sessionId - Specific session ID to resume
|
||||
* @returns {Promise<Object>} Execution result
|
||||
*/
|
||||
static async execute(profile, enhancedPrompt, options = {}) {
|
||||
const {
|
||||
cwd = process.cwd(),
|
||||
timeout = 600000, // 10 minutes default
|
||||
outputFormat = 'stream-json', // Use stream-json for real-time progress
|
||||
permissionMode = 'acceptEdits',
|
||||
resumeSession = false,
|
||||
sessionId = null
|
||||
} = options;
|
||||
|
||||
// Validate permission mode
|
||||
this._validatePermissionMode(permissionMode);
|
||||
|
||||
// Initialize session manager
|
||||
const sessionMgr = new SessionManager();
|
||||
|
||||
// Detect Claude CLI path
|
||||
const claudeCli = this._detectClaudeCli();
|
||||
if (!claudeCli) {
|
||||
throw new Error('Claude CLI not found in PATH. Install from: https://docs.claude.com/en/docs/claude-code/installation');
|
||||
}
|
||||
|
||||
// Get settings path for profile
|
||||
const settingsPath = path.join(os.homedir(), '.ccs', `${profile}.settings.json`);
|
||||
|
||||
// Validate settings file exists
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
throw new Error(`Settings file not found: ${settingsPath}\nProfile "${profile}" may not be configured.`);
|
||||
}
|
||||
|
||||
// Smart slash command detection and preservation
|
||||
// Detects if prompt contains slash command and restructures for proper execution
|
||||
const processedPrompt = this._processSlashCommand(enhancedPrompt);
|
||||
|
||||
// Prepare arguments
|
||||
const args = ['-p', processedPrompt, '--settings', settingsPath];
|
||||
|
||||
// Always use stream-json for real-time progress visibility
|
||||
// Note: --verbose is required when using --print with stream-json
|
||||
args.push('--output-format', 'stream-json', '--verbose');
|
||||
|
||||
// Add permission mode
|
||||
if (permissionMode && permissionMode !== 'default') {
|
||||
if (permissionMode === 'bypassPermissions') {
|
||||
args.push('--dangerously-skip-permissions');
|
||||
// Warn about dangerous mode
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.warn('[!] WARNING: Using --dangerously-skip-permissions mode');
|
||||
console.warn('[!] This bypasses ALL permission checks. Use only in trusted environments.');
|
||||
}
|
||||
} else {
|
||||
args.push('--permission-mode', permissionMode);
|
||||
}
|
||||
}
|
||||
|
||||
// Add resume flag for multi-turn sessions
|
||||
if (resumeSession) {
|
||||
const lastSession = sessionMgr.getLastSession(profile);
|
||||
|
||||
if (lastSession) {
|
||||
args.push('--resume', lastSession.sessionId);
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[i] Resuming session: ${lastSession.sessionId} (${lastSession.turns} turns, $${lastSession.totalCost.toFixed(4)})`);
|
||||
}
|
||||
} else if (sessionId) {
|
||||
args.push('--resume', sessionId);
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[i] Resuming specific session: ${sessionId}`);
|
||||
}
|
||||
} else {
|
||||
console.warn('[!] No previous session found, starting new session');
|
||||
}
|
||||
} else if (sessionId) {
|
||||
args.push('--resume', sessionId);
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[i] Resuming specific session: ${sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add tool restrictions from settings
|
||||
const toolRestrictions = SettingsParser.parseToolRestrictions(cwd);
|
||||
|
||||
if (toolRestrictions.allowedTools.length > 0) {
|
||||
args.push('--allowedTools');
|
||||
toolRestrictions.allowedTools.forEach(tool => args.push(tool));
|
||||
}
|
||||
|
||||
if (toolRestrictions.disallowedTools.length > 0) {
|
||||
args.push('--disallowedTools');
|
||||
toolRestrictions.disallowedTools.forEach(tool => args.push(tool));
|
||||
}
|
||||
|
||||
// Note: No max-turns limit - using time-based limits instead (default 10min timeout)
|
||||
|
||||
// Debug log args
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[i] Claude CLI args: ${args.join(' ')}`);
|
||||
}
|
||||
|
||||
// Execute with spawn
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Show progress unless explicitly disabled with CCS_QUIET
|
||||
const showProgress = !process.env.CCS_QUIET;
|
||||
|
||||
// Show initial progress message
|
||||
if (showProgress) {
|
||||
const modelName = profile === 'glm' ? 'GLM-4.6' : profile === 'kimi' ? 'Kimi' : profile.toUpperCase();
|
||||
console.error(`[i] Delegating to ${modelName}...`);
|
||||
}
|
||||
|
||||
const proc = spawn(claudeCli, args, {
|
||||
cwd,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let progressInterval;
|
||||
const messages = []; // Accumulate stream-json messages
|
||||
let partialLine = ''; // Buffer for incomplete JSON lines
|
||||
|
||||
// Handle parent process termination (Ctrl+C or Esc in Claude)
|
||||
// When main Claude session is killed, cleanup spawned child process
|
||||
const cleanupHandler = () => {
|
||||
if (!proc.killed) {
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error('[!] Parent process terminating, killing delegated session...');
|
||||
}
|
||||
proc.kill('SIGTERM');
|
||||
// Force kill if not dead after 2s
|
||||
setTimeout(() => {
|
||||
if (!proc.killed) {
|
||||
proc.kill('SIGKILL');
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// Register signal handlers for parent process termination
|
||||
process.once('SIGINT', cleanupHandler);
|
||||
process.once('SIGTERM', cleanupHandler);
|
||||
|
||||
// Cleanup signal handlers when child process exits
|
||||
const removeSignalHandlers = () => {
|
||||
process.removeListener('SIGINT', cleanupHandler);
|
||||
process.removeListener('SIGTERM', cleanupHandler);
|
||||
};
|
||||
|
||||
proc.on('close', removeSignalHandlers);
|
||||
proc.on('error', removeSignalHandlers);
|
||||
|
||||
// Progress indicator (show elapsed time every 5 seconds)
|
||||
if (showProgress) {
|
||||
progressInterval = setInterval(() => {
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
process.stderr.write(`[i] Still running... ${elapsed}s elapsed\r`);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Capture stdout (stream-json format - jsonl)
|
||||
proc.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
|
||||
// Parse stream-json messages (jsonl format - one JSON per line)
|
||||
const chunk = partialLine + data.toString();
|
||||
const lines = chunk.split('\n');
|
||||
partialLine = lines.pop() || ''; // Save incomplete line for next chunk
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
try {
|
||||
const msg = JSON.parse(line);
|
||||
messages.push(msg);
|
||||
|
||||
// Show real-time tool use with verbose details
|
||||
if (showProgress && msg.type === 'assistant') {
|
||||
const toolUses = msg.message?.content?.filter(c => c.type === 'tool_use') || [];
|
||||
|
||||
for (const tool of toolUses) {
|
||||
process.stderr.write('\r\x1b[K'); // Clear line
|
||||
|
||||
// Show verbose tool use with description/input if available
|
||||
const toolInput = tool.input || {};
|
||||
let verboseMsg = `[Tool] ${tool.name}`;
|
||||
|
||||
// Add context based on tool type (all Claude Code tools)
|
||||
switch (tool.name) {
|
||||
case 'Bash':
|
||||
if (toolInput.command) {
|
||||
// Truncate long commands
|
||||
const cmd = toolInput.command.length > 80
|
||||
? toolInput.command.substring(0, 77) + '...'
|
||||
: toolInput.command;
|
||||
verboseMsg += `: ${cmd}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Edit':
|
||||
case 'Write':
|
||||
case 'Read':
|
||||
if (toolInput.file_path) {
|
||||
verboseMsg += `: ${toolInput.file_path}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NotebookEdit':
|
||||
case 'NotebookRead':
|
||||
if (toolInput.notebook_path) {
|
||||
verboseMsg += `: ${toolInput.notebook_path}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Grep':
|
||||
if (toolInput.pattern) {
|
||||
verboseMsg += `: searching for "${toolInput.pattern}"`;
|
||||
if (toolInput.path) {
|
||||
verboseMsg += ` in ${toolInput.path}`;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Glob':
|
||||
if (toolInput.pattern) {
|
||||
verboseMsg += `: ${toolInput.pattern}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'SlashCommand':
|
||||
if (toolInput.command) {
|
||||
verboseMsg += `: ${toolInput.command}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Task':
|
||||
if (toolInput.description) {
|
||||
verboseMsg += `: ${toolInput.description}`;
|
||||
} else if (toolInput.prompt) {
|
||||
const prompt = toolInput.prompt.length > 60
|
||||
? toolInput.prompt.substring(0, 57) + '...'
|
||||
: toolInput.prompt;
|
||||
verboseMsg += `: ${prompt}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'TodoWrite':
|
||||
if (toolInput.todos && Array.isArray(toolInput.todos)) {
|
||||
// Show in_progress task instead of just count
|
||||
const inProgressTask = toolInput.todos.find(t => t.status === 'in_progress');
|
||||
if (inProgressTask && inProgressTask.activeForm) {
|
||||
verboseMsg += `: ${inProgressTask.activeForm}`;
|
||||
} else {
|
||||
// Fallback to count if no in_progress task
|
||||
verboseMsg += `: ${toolInput.todos.length} task(s)`;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'WebFetch':
|
||||
if (toolInput.url) {
|
||||
verboseMsg += `: ${toolInput.url}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'WebSearch':
|
||||
if (toolInput.query) {
|
||||
verboseMsg += `: "${toolInput.query}"`;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// For unknown tools, show first meaningful parameter
|
||||
if (Object.keys(toolInput).length > 0) {
|
||||
const firstKey = Object.keys(toolInput)[0];
|
||||
const firstValue = toolInput[firstKey];
|
||||
if (typeof firstValue === 'string' && firstValue.length < 60) {
|
||||
verboseMsg += `: ${firstValue}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.stderr.write(`${verboseMsg}\n`);
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Skip malformed JSON lines (shouldn't happen with stream-json)
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[!] Failed to parse stream-json line: ${parseError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Stream stderr in real-time (progress messages from Claude CLI)
|
||||
proc.stderr.on('data', (data) => {
|
||||
const stderrText = data.toString();
|
||||
stderr += stderrText;
|
||||
|
||||
// Show stderr in real-time if in TTY
|
||||
if (showProgress) {
|
||||
// Clear progress line before showing stderr
|
||||
if (progressInterval) {
|
||||
process.stderr.write('\r\x1b[K'); // Clear line
|
||||
}
|
||||
process.stderr.write(stderrText);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle completion
|
||||
proc.on('close', (exitCode) => {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Clear progress indicator
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
process.stderr.write('\r\x1b[K'); // Clear line
|
||||
}
|
||||
|
||||
// Show completion message
|
||||
if (showProgress) {
|
||||
const durationSec = (duration / 1000).toFixed(1);
|
||||
if (timedOut) {
|
||||
console.error(`[i] Execution timed out after ${durationSec}s`);
|
||||
} else {
|
||||
console.error(`[i] Execution completed in ${durationSec}s`);
|
||||
}
|
||||
console.error(''); // Blank line before formatted output
|
||||
}
|
||||
|
||||
const result = {
|
||||
exitCode,
|
||||
stdout,
|
||||
stderr,
|
||||
cwd,
|
||||
profile,
|
||||
duration,
|
||||
timedOut,
|
||||
success: exitCode === 0 && !timedOut,
|
||||
messages // Include all stream-json messages
|
||||
};
|
||||
|
||||
// Extract metadata from final 'result' message in stream-json
|
||||
const resultMessage = messages.find(m => m.type === 'result');
|
||||
if (resultMessage) {
|
||||
// Add parsed fields from result message
|
||||
result.sessionId = resultMessage.session_id || null;
|
||||
result.totalCost = resultMessage.total_cost_usd || 0;
|
||||
result.numTurns = resultMessage.num_turns || 0;
|
||||
result.isError = resultMessage.is_error || false;
|
||||
result.type = resultMessage.type || null;
|
||||
result.subtype = resultMessage.subtype || null;
|
||||
result.durationApi = resultMessage.duration_api_ms || 0;
|
||||
result.permissionDenials = resultMessage.permission_denials || [];
|
||||
result.errors = resultMessage.errors || [];
|
||||
|
||||
// Extract content from result message
|
||||
result.content = resultMessage.result || '';
|
||||
} else {
|
||||
// Fallback: no result message found (shouldn't happen)
|
||||
result.content = stdout;
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[!] No result message found in stream-json output`);
|
||||
}
|
||||
}
|
||||
|
||||
// Store or update session if we have session ID (even on timeout, for :continue support)
|
||||
if (result.sessionId) {
|
||||
if (resumeSession || sessionId) {
|
||||
// Update existing session
|
||||
sessionMgr.updateSession(profile, result.sessionId, {
|
||||
totalCost: result.totalCost
|
||||
});
|
||||
} else {
|
||||
// Store new session
|
||||
sessionMgr.storeSession(profile, {
|
||||
sessionId: result.sessionId,
|
||||
totalCost: result.totalCost,
|
||||
cwd: result.cwd
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup expired sessions periodically
|
||||
if (Math.random() < 0.1) { // 10% chance
|
||||
sessionMgr.cleanupExpired();
|
||||
}
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
proc.on('error', (error) => {
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
reject(new Error(`Failed to execute Claude CLI: ${error.message}`));
|
||||
});
|
||||
|
||||
// Handle timeout with graceful SIGTERM then forceful SIGKILL
|
||||
let timedOut = false;
|
||||
if (timeout > 0) {
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
if (!proc.killed) {
|
||||
timedOut = true;
|
||||
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
process.stderr.write('\r\x1b[K'); // Clear line
|
||||
}
|
||||
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[!] Timeout reached after ${timeout}ms, sending SIGTERM for graceful shutdown...`);
|
||||
}
|
||||
|
||||
// Send SIGTERM for graceful shutdown
|
||||
proc.kill('SIGTERM');
|
||||
|
||||
// If process doesn't terminate within 10s, force kill
|
||||
setTimeout(() => {
|
||||
if (!proc.killed) {
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[!] Process did not terminate gracefully, sending SIGKILL...`);
|
||||
}
|
||||
proc.kill('SIGKILL');
|
||||
}
|
||||
}, 10000); // Give 10s for graceful shutdown instead of 5s
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
// Clear timeout on successful completion
|
||||
proc.on('close', () => clearTimeout(timeoutHandle));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate permission mode
|
||||
* @param {string} mode - Permission mode
|
||||
* @throws {Error} If mode is invalid
|
||||
* @private
|
||||
*/
|
||||
static _validatePermissionMode(mode) {
|
||||
const VALID_MODES = ['default', 'plan', 'acceptEdits', 'bypassPermissions'];
|
||||
if (!VALID_MODES.includes(mode)) {
|
||||
throw new Error(
|
||||
`Invalid permission mode: "${mode}". Valid modes: ${VALID_MODES.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect Claude CLI executable
|
||||
* @returns {string|null} Path to claude CLI or null if not found
|
||||
* @private
|
||||
*/
|
||||
static _detectClaudeCli() {
|
||||
// Check environment variable override
|
||||
if (process.env.CCS_CLAUDE_PATH) {
|
||||
return process.env.CCS_CLAUDE_PATH;
|
||||
}
|
||||
|
||||
// Try to find in PATH
|
||||
const { execSync } = require('child_process');
|
||||
try {
|
||||
const result = execSync('command -v claude', { encoding: 'utf8' });
|
||||
return result.trim();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute with retry logic
|
||||
* @param {string} profile - Profile name
|
||||
* @param {string} enhancedPrompt - Enhanced prompt
|
||||
* @param {Object} options - Execution options
|
||||
* @param {number} options.maxRetries - Maximum retry attempts (default: 2)
|
||||
* @returns {Promise<Object>} Execution result
|
||||
*/
|
||||
static async executeWithRetry(profile, enhancedPrompt, options = {}) {
|
||||
const { maxRetries = 2, ...execOptions } = options;
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const result = await this.execute(profile, enhancedPrompt, execOptions);
|
||||
|
||||
// If successful, return immediately
|
||||
if (result.success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// If not last attempt, retry
|
||||
if (attempt < maxRetries) {
|
||||
console.error(`[!] Attempt ${attempt + 1} failed, retrying...`);
|
||||
await this._sleep(1000 * (attempt + 1)); // Exponential backoff
|
||||
continue;
|
||||
}
|
||||
|
||||
// Last attempt failed, return result anyway
|
||||
return result;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
console.error(`[!] Attempt ${attempt + 1} errored: ${error.message}, retrying...`);
|
||||
await this._sleep(1000 * (attempt + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All retries exhausted
|
||||
throw lastError || new Error('Execution failed after all retry attempts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep utility for retry backoff
|
||||
* @param {number} ms - Milliseconds to sleep
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
static _sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process prompt to detect and preserve slash commands
|
||||
* Implements smart enhancement: preserves slash command at start, allows context in rest
|
||||
* @param {string} prompt - Original prompt (may contain slash command)
|
||||
* @returns {string} Processed prompt with slash command preserved
|
||||
* @private
|
||||
*/
|
||||
static _processSlashCommand(prompt) {
|
||||
const trimmed = prompt.trim();
|
||||
|
||||
// Case 1: Already starts with slash command - keep as-is
|
||||
if (trimmed.match(/^\/[\w:-]+(\s|$)/)) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
// Case 2: Find slash command embedded in text
|
||||
// Look for /command that's NOT part of a file path
|
||||
// File paths: /home/user, /path/to/file (have / before or after)
|
||||
// Commands: /cook, /plan (standalone, preceded by space/colon/start)
|
||||
// Strategy: Find LAST occurrence that looks like a command, not a path
|
||||
const embeddedSlash = trimmed.match(/(?:^|[^\w/])(\/[\w:-]+)(\s+[\s\S]*)?$/);
|
||||
|
||||
if (embeddedSlash) {
|
||||
const command = embeddedSlash[1]; // e.g., "/cook"
|
||||
const args = (embeddedSlash[2] || '').trim(); // Everything after command
|
||||
|
||||
// Calculate where the command starts (excluding preceding char if any)
|
||||
const matchStart = embeddedSlash.index + (embeddedSlash[0][0] === '/' ? 0 : 1);
|
||||
const beforeCommand = trimmed.substring(0, matchStart).trim();
|
||||
|
||||
// Restructure: command first, context after
|
||||
if (beforeCommand && args) {
|
||||
return `${command} ${args}\n\nContext: ${beforeCommand}`;
|
||||
} else if (beforeCommand) {
|
||||
return `${command}\n\nContext: ${beforeCommand}`;
|
||||
}
|
||||
return args ? `${command} ${args}` : command;
|
||||
}
|
||||
|
||||
// No slash command detected, return as-is
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if profile is executable (quick health check)
|
||||
* @param {string} profile - Profile name
|
||||
* @returns {Promise<boolean>} True if profile can execute
|
||||
*/
|
||||
static async testProfile(profile) {
|
||||
try {
|
||||
const result = await this.execute(profile, 'Say "test successful"', {
|
||||
timeout: 10000
|
||||
});
|
||||
return result.success;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HeadlessExecutor };
|
||||
@@ -0,0 +1,483 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Formats delegation execution results for display
|
||||
* Creates ASCII box output with file change tracking
|
||||
*/
|
||||
class ResultFormatter {
|
||||
/**
|
||||
* Format execution result with complete source-of-truth
|
||||
* @param {Object} result - Execution result from HeadlessExecutor
|
||||
* @param {string} result.profile - Profile used (glm, kimi, etc.)
|
||||
* @param {string} result.cwd - Working directory
|
||||
* @param {number} result.exitCode - Exit code
|
||||
* @param {string} result.stdout - Standard output
|
||||
* @param {string} result.stderr - Standard error
|
||||
* @param {number} result.duration - Duration in milliseconds
|
||||
* @param {boolean} result.success - Success flag
|
||||
* @param {string} result.content - Parsed content (from JSON or stdout)
|
||||
* @param {string} result.sessionId - Session ID (from JSON)
|
||||
* @param {number} result.totalCost - Total cost USD (from JSON)
|
||||
* @param {number} result.numTurns - Number of turns (from JSON)
|
||||
* @returns {string} Formatted result
|
||||
*/
|
||||
static format(result) {
|
||||
const { profile, cwd, exitCode, stdout, stderr, duration, success, content, sessionId, totalCost, numTurns, subtype, permissionDenials, errors, json, timedOut } = result;
|
||||
|
||||
// Handle timeout (graceful termination)
|
||||
if (timedOut) {
|
||||
return this._formatTimeoutError(result);
|
||||
}
|
||||
|
||||
// Handle legacy max_turns error (Claude CLI might still return this)
|
||||
if (subtype === 'error_max_turns') {
|
||||
return this._formatTimeoutError(result);
|
||||
}
|
||||
|
||||
// Use content field for output (JSON result or fallback stdout)
|
||||
const displayOutput = content || stdout;
|
||||
|
||||
// Build formatted output
|
||||
let output = '';
|
||||
|
||||
// Header
|
||||
output += this._formatHeader(profile, success);
|
||||
|
||||
// Info box (file detection handled by delegated session itself)
|
||||
output += this._formatInfoBox(cwd, profile, duration, exitCode, sessionId, totalCost, numTurns);
|
||||
|
||||
// Task output
|
||||
output += '\n';
|
||||
output += this._formatOutput(displayOutput);
|
||||
|
||||
// Permission denials if present
|
||||
if (permissionDenials && permissionDenials.length > 0) {
|
||||
output += '\n';
|
||||
output += this._formatPermissionDenials(permissionDenials);
|
||||
}
|
||||
|
||||
// Errors if present
|
||||
if (errors && errors.length > 0) {
|
||||
output += '\n';
|
||||
output += this._formatErrors(errors);
|
||||
}
|
||||
|
||||
// Stderr if present
|
||||
if (stderr && stderr.trim()) {
|
||||
output += '\n';
|
||||
output += this._formatStderr(stderr);
|
||||
}
|
||||
|
||||
// Footer
|
||||
output += '\n';
|
||||
output += this._formatFooter(success, duration);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract file changes from output
|
||||
* @param {string} output - Command output
|
||||
* @param {string} cwd - Working directory for filesystem scanning fallback
|
||||
* @returns {Object} { created: Array<string>, modified: Array<string> }
|
||||
*/
|
||||
static extractFileChanges(output, cwd) {
|
||||
const created = [];
|
||||
const modified = [];
|
||||
|
||||
// Patterns to match file operations (case-insensitive)
|
||||
const createdPatterns = [
|
||||
/created:\s*([^\n\r]+)/gi,
|
||||
/create:\s*([^\n\r]+)/gi,
|
||||
/wrote:\s*([^\n\r]+)/gi,
|
||||
/write:\s*([^\n\r]+)/gi,
|
||||
/new file:\s*([^\n\r]+)/gi,
|
||||
/generated:\s*([^\n\r]+)/gi,
|
||||
/added:\s*([^\n\r]+)/gi
|
||||
];
|
||||
|
||||
const modifiedPatterns = [
|
||||
/modified:\s*([^\n\r]+)/gi,
|
||||
/update:\s*([^\n\r]+)/gi,
|
||||
/updated:\s*([^\n\r]+)/gi,
|
||||
/edit:\s*([^\n\r]+)/gi,
|
||||
/edited:\s*([^\n\r]+)/gi,
|
||||
/changed:\s*([^\n\r]+)/gi
|
||||
];
|
||||
|
||||
// Helper to check if file is infrastructure (should be ignored)
|
||||
const isInfrastructure = (filePath) => {
|
||||
return filePath.includes('/.claude/') || filePath.startsWith('.claude/');
|
||||
};
|
||||
|
||||
// Extract created files
|
||||
for (const pattern of createdPatterns) {
|
||||
let match;
|
||||
while ((match = pattern.exec(output)) !== null) {
|
||||
const filePath = match[1].trim();
|
||||
if (filePath && !created.includes(filePath) && !isInfrastructure(filePath)) {
|
||||
created.push(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract modified files
|
||||
for (const pattern of modifiedPatterns) {
|
||||
let match;
|
||||
while ((match = pattern.exec(output)) !== null) {
|
||||
const filePath = match[1].trim();
|
||||
// Don't include if already in created list or is infrastructure
|
||||
if (filePath && !modified.includes(filePath) && !created.includes(filePath) && !isInfrastructure(filePath)) {
|
||||
modified.push(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Scan filesystem for recently modified files (last 5 minutes)
|
||||
if (created.length === 0 && modified.length === 0 && cwd) {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const childProcess = require('child_process');
|
||||
|
||||
// Use find command to get recently modified files (excluding infrastructure)
|
||||
const findCmd = `find . -type f -mmin -5 -not -path "./.git/*" -not -path "./node_modules/*" -not -path "./.claude/*" 2>/dev/null | head -20`;
|
||||
const result = childProcess.execSync(findCmd, { cwd, encoding: 'utf8', timeout: 5000 });
|
||||
|
||||
const files = result.split('\n').filter(f => f.trim());
|
||||
files.forEach(file => {
|
||||
const fullPath = path.join(cwd, file);
|
||||
|
||||
// Double-check not infrastructure
|
||||
if (isInfrastructure(fullPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = fs.statSync(fullPath);
|
||||
const now = Date.now();
|
||||
const mtime = stats.mtimeMs;
|
||||
const ctime = stats.ctimeMs;
|
||||
|
||||
// If both mtime and ctime are very recent (within 10 minutes), likely created
|
||||
// ctime = inode change time, for new files this is close to creation time
|
||||
const isVeryRecent = (now - mtime) < 600000 && (now - ctime) < 600000;
|
||||
const timeDiff = Math.abs(mtime - ctime);
|
||||
|
||||
// If mtime and ctime are very close (< 1 second apart) and both recent, it's created
|
||||
if (isVeryRecent && timeDiff < 1000) {
|
||||
if (!created.includes(fullPath)) {
|
||||
created.push(fullPath);
|
||||
}
|
||||
} else {
|
||||
// Otherwise, it's modified
|
||||
if (!modified.includes(fullPath)) {
|
||||
modified.push(fullPath);
|
||||
}
|
||||
}
|
||||
} catch (statError) {
|
||||
// If stat fails, default to created (since we're in fallback mode)
|
||||
if (!created.includes(fullPath) && !modified.includes(fullPath)) {
|
||||
created.push(fullPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (scanError) {
|
||||
// Silently fail if filesystem scan doesn't work
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[!] Filesystem scan failed: ${scanError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { created, modified };
|
||||
}
|
||||
|
||||
/**
|
||||
* Format header with delegation indicator
|
||||
* @param {string} profile - Profile name
|
||||
* @param {boolean} success - Success flag
|
||||
* @returns {string} Formatted header
|
||||
* @private
|
||||
*/
|
||||
static _formatHeader(profile, success) {
|
||||
const modelName = this._getModelDisplayName(profile);
|
||||
const icon = success ? '[i]' : '[X]';
|
||||
return `${icon} Delegated to ${modelName} (ccs:${profile})\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format info box with delegation details
|
||||
* @param {string} cwd - Working directory
|
||||
* @param {string} profile - Profile name
|
||||
* @param {number} duration - Duration in ms
|
||||
* @param {number} exitCode - Exit code
|
||||
* @param {string} sessionId - Session ID (from JSON)
|
||||
* @param {number} totalCost - Total cost USD (from JSON)
|
||||
* @param {number} numTurns - Number of turns (from JSON)
|
||||
* @returns {string} Formatted info box
|
||||
* @private
|
||||
*/
|
||||
static _formatInfoBox(cwd, profile, duration, exitCode, sessionId, totalCost, numTurns) {
|
||||
const modelName = this._getModelDisplayName(profile);
|
||||
const durationSec = (duration / 1000).toFixed(1);
|
||||
|
||||
// Calculate box width (fit longest line + padding)
|
||||
const maxWidth = 70;
|
||||
const cwdLine = `Working Directory: ${cwd}`;
|
||||
const boxWidth = Math.min(Math.max(cwdLine.length + 4, 50), maxWidth);
|
||||
|
||||
const lines = [
|
||||
`Working Directory: ${this._truncate(cwd, boxWidth - 22)}`,
|
||||
`Model: ${modelName}`,
|
||||
`Duration: ${durationSec}s`,
|
||||
`Exit Code: ${exitCode}`
|
||||
];
|
||||
|
||||
// Add JSON-specific fields if available
|
||||
if (sessionId) {
|
||||
// Abbreviate session ID (Git-style first 8 chars) to prevent wrapping
|
||||
const shortId = sessionId.length > 8 ? sessionId.substring(0, 8) : sessionId;
|
||||
lines.push(`Session ID: ${shortId}`);
|
||||
}
|
||||
if (totalCost !== undefined && totalCost !== null) {
|
||||
lines.push(`Cost: $${totalCost.toFixed(4)}`);
|
||||
}
|
||||
if (numTurns) {
|
||||
lines.push(`Turns: ${numTurns}`);
|
||||
}
|
||||
|
||||
let box = '';
|
||||
box += '╔' + '═'.repeat(boxWidth - 2) + '╗\n';
|
||||
|
||||
for (const line of lines) {
|
||||
const padding = boxWidth - line.length - 4;
|
||||
box += '║ ' + line + ' '.repeat(Math.max(0, padding)) + ' ║\n';
|
||||
}
|
||||
|
||||
box += '╚' + '═'.repeat(boxWidth - 2) + '╝';
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format task output
|
||||
* @param {string} output - Standard output
|
||||
* @returns {string} Formatted output
|
||||
* @private
|
||||
*/
|
||||
static _formatOutput(output) {
|
||||
if (!output || !output.trim()) {
|
||||
return '[i] No output from delegated task\n';
|
||||
}
|
||||
|
||||
return output.trim() + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format stderr output
|
||||
* @param {string} stderr - Standard error
|
||||
* @returns {string} Formatted stderr
|
||||
* @private
|
||||
*/
|
||||
static _formatStderr(stderr) {
|
||||
return `[!] Stderr:\n${stderr.trim()}\n\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file list (created or modified)
|
||||
* @param {string} label - Label (Created/Modified)
|
||||
* @param {Array<string>} files - File paths
|
||||
* @returns {string} Formatted file list
|
||||
* @private
|
||||
*/
|
||||
static _formatFileList(label, files) {
|
||||
let output = `[i] ${label} Files:\n`;
|
||||
|
||||
for (const file of files) {
|
||||
output += ` - ${file}\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format footer with completion status
|
||||
* @param {boolean} success - Success flag
|
||||
* @param {number} duration - Duration in ms
|
||||
* @returns {string} Formatted footer
|
||||
* @private
|
||||
*/
|
||||
static _formatFooter(success, duration) {
|
||||
const icon = success ? '[OK]' : '[X]';
|
||||
const status = success ? 'Delegation completed' : 'Delegation failed';
|
||||
return `${icon} ${status}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for model profile
|
||||
* @param {string} profile - Profile name
|
||||
* @returns {string} Display name
|
||||
* @private
|
||||
*/
|
||||
static _getModelDisplayName(profile) {
|
||||
const displayNames = {
|
||||
'glm': 'GLM-4.6',
|
||||
'glmt': 'GLM-4.6 (Thinking)',
|
||||
'kimi': 'Kimi',
|
||||
'default': 'Claude'
|
||||
};
|
||||
|
||||
return displayNames[profile] || profile.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate string to max length
|
||||
* @param {string} str - String to truncate
|
||||
* @param {number} maxLength - Maximum length
|
||||
* @returns {string} Truncated string
|
||||
* @private
|
||||
*/
|
||||
static _truncate(str, maxLength) {
|
||||
if (str.length <= maxLength) {
|
||||
return str;
|
||||
}
|
||||
return str.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format minimal result (for quick tasks)
|
||||
* @param {Object} result - Execution result
|
||||
* @returns {string} Minimal formatted result
|
||||
*/
|
||||
static formatMinimal(result) {
|
||||
const { profile, success, duration } = result;
|
||||
const modelName = this._getModelDisplayName(profile);
|
||||
const icon = success ? '[OK]' : '[X]';
|
||||
const durationSec = (duration / 1000).toFixed(1);
|
||||
|
||||
return `${icon} ${modelName} delegation ${success ? 'completed' : 'failed'} (${durationSec}s)\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format verbose result (with full details)
|
||||
* @param {Object} result - Execution result
|
||||
* @returns {string} Verbose formatted result
|
||||
*/
|
||||
static formatVerbose(result) {
|
||||
const basic = this.format(result);
|
||||
|
||||
// Add additional debug info
|
||||
let verbose = basic;
|
||||
verbose += '\n=== Debug Information ===\n';
|
||||
verbose += `CWD: ${result.cwd}\n`;
|
||||
verbose += `Profile: ${result.profile}\n`;
|
||||
verbose += `Exit Code: ${result.exitCode}\n`;
|
||||
verbose += `Duration: ${result.duration}ms\n`;
|
||||
verbose += `Success: ${result.success}\n`;
|
||||
verbose += `Stdout Length: ${result.stdout.length} chars\n`;
|
||||
verbose += `Stderr Length: ${result.stderr.length} chars\n`;
|
||||
|
||||
return verbose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if NO_COLOR environment variable is set
|
||||
* @returns {boolean} True if colors should be disabled
|
||||
* @private
|
||||
*/
|
||||
static _shouldDisableColors() {
|
||||
return process.env.NO_COLOR !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timeout error (session exceeded time limit)
|
||||
* @param {Object} result - Execution result
|
||||
* @returns {string} Formatted timeout error
|
||||
* @private
|
||||
*/
|
||||
static _formatTimeoutError(result) {
|
||||
const { profile, cwd, duration, sessionId, totalCost, numTurns, permissionDenials } = result;
|
||||
|
||||
let output = '';
|
||||
|
||||
// Header
|
||||
output += this._formatHeader(profile, false);
|
||||
|
||||
// Info box
|
||||
output += this._formatInfoBox(cwd, profile, duration, 0, sessionId, totalCost, numTurns);
|
||||
|
||||
// Timeout message
|
||||
output += '\n';
|
||||
const timeoutMin = (duration / 60000).toFixed(1);
|
||||
output += `[!] Execution timed out after ${timeoutMin} minutes\n\n`;
|
||||
output += 'The delegated session exceeded its time limit before completing the task.\n';
|
||||
output += 'Session was gracefully terminated and saved for continuation.\n';
|
||||
|
||||
// Permission denials if present
|
||||
if (permissionDenials && permissionDenials.length > 0) {
|
||||
output += '\n';
|
||||
output += this._formatPermissionDenials(permissionDenials);
|
||||
output += '\n';
|
||||
output += 'The task may require permissions that were denied.\n';
|
||||
output += 'Consider running with --permission-mode bypassPermissions or execute manually.\n';
|
||||
}
|
||||
|
||||
// Suggestions
|
||||
output += '\n';
|
||||
output += 'Suggestions:\n';
|
||||
output += ` - Continue session: ccs ${profile}:continue -p "finish the task"\n`;
|
||||
output += ` - Increase timeout: ccs ${profile} -p "task" --timeout ${duration * 2}\n`;
|
||||
output += ' - Break task into smaller steps\n';
|
||||
output += ' - Run task manually in main Claude session\n';
|
||||
|
||||
output += '\n';
|
||||
// Abbreviate session ID (Git-style first 8 chars)
|
||||
const shortId = sessionId && sessionId.length > 8 ? sessionId.substring(0, 8) : sessionId;
|
||||
output += `[i] Session persisted with ID: ${shortId}\n`;
|
||||
output += `[i] Cost: $${totalCost.toFixed(4)}\n`;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format permission denials
|
||||
* @param {Array<Object>} denials - Permission denial objects
|
||||
* @returns {string} Formatted permission denials
|
||||
* @private
|
||||
*/
|
||||
static _formatPermissionDenials(denials) {
|
||||
let output = '[!] Permission Denials:\n';
|
||||
|
||||
for (const denial of denials) {
|
||||
const tool = denial.tool_name || 'Unknown';
|
||||
const input = denial.tool_input || {};
|
||||
const command = input.command || input.description || JSON.stringify(input);
|
||||
|
||||
output += ` - ${tool}: ${command}\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format errors array
|
||||
* @param {Array<Object>} errors - Error objects
|
||||
* @returns {string} Formatted errors
|
||||
* @private
|
||||
*/
|
||||
static _formatErrors(errors) {
|
||||
let output = '[X] Errors:\n';
|
||||
|
||||
for (const error of errors) {
|
||||
const message = error.message || error.error || JSON.stringify(error);
|
||||
output += ` - ${message}\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ResultFormatter };
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* Manages delegation session persistence for multi-turn conversations
|
||||
*/
|
||||
class SessionManager {
|
||||
constructor() {
|
||||
this.sessionsPath = path.join(os.homedir(), '.ccs', 'delegation-sessions.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store new session metadata
|
||||
* @param {string} profile - Profile name (glm, kimi, etc.)
|
||||
* @param {Object} sessionData - Session data
|
||||
* @param {string} sessionData.sessionId - Claude session ID
|
||||
* @param {number} sessionData.totalCost - Initial cost
|
||||
* @param {string} sessionData.cwd - Working directory
|
||||
*/
|
||||
storeSession(profile, sessionData) {
|
||||
const sessions = this._loadSessions();
|
||||
const key = `${profile}:latest`;
|
||||
|
||||
sessions[key] = {
|
||||
sessionId: sessionData.sessionId,
|
||||
profile,
|
||||
startTime: Date.now(),
|
||||
lastTurnTime: Date.now(),
|
||||
totalCost: sessionData.totalCost || 0,
|
||||
turns: 1,
|
||||
cwd: sessionData.cwd || process.cwd()
|
||||
};
|
||||
|
||||
this._saveSessions(sessions);
|
||||
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[i] Stored session: ${sessionData.sessionId} for ${profile}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session after additional turn
|
||||
* @param {string} profile - Profile name
|
||||
* @param {string} sessionId - Session ID
|
||||
* @param {Object} turnData - Turn data
|
||||
* @param {number} turnData.totalCost - Turn cost
|
||||
*/
|
||||
updateSession(profile, sessionId, turnData) {
|
||||
const sessions = this._loadSessions();
|
||||
const key = `${profile}:latest`;
|
||||
|
||||
if (sessions[key]?.sessionId === sessionId) {
|
||||
sessions[key].lastTurnTime = Date.now();
|
||||
sessions[key].totalCost += turnData.totalCost || 0;
|
||||
sessions[key].turns += 1;
|
||||
this._saveSessions(sessions);
|
||||
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[i] Updated session: ${sessionId}, total: $${sessions[key].totalCost.toFixed(4)}, turns: ${sessions[key].turns}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last session for profile
|
||||
* @param {string} profile - Profile name
|
||||
* @returns {Object|null} Session metadata or null
|
||||
*/
|
||||
getLastSession(profile) {
|
||||
const sessions = this._loadSessions();
|
||||
const key = `${profile}:latest`;
|
||||
return sessions[key] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all sessions for profile
|
||||
* @param {string} profile - Profile name
|
||||
*/
|
||||
clearProfile(profile) {
|
||||
const sessions = this._loadSessions();
|
||||
const key = `${profile}:latest`;
|
||||
delete sessions[key];
|
||||
this._saveSessions(sessions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up expired sessions (>30 days)
|
||||
*/
|
||||
cleanupExpired() {
|
||||
const sessions = this._loadSessions();
|
||||
const now = Date.now();
|
||||
const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
|
||||
|
||||
let cleaned = 0;
|
||||
Object.keys(sessions).forEach(key => {
|
||||
if (now - sessions[key].lastTurnTime > maxAge) {
|
||||
delete sessions[key];
|
||||
cleaned++;
|
||||
}
|
||||
});
|
||||
|
||||
if (cleaned > 0) {
|
||||
this._saveSessions(sessions);
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[i] Cleaned ${cleaned} expired sessions`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load sessions from disk
|
||||
* @returns {Object} Sessions object
|
||||
* @private
|
||||
*/
|
||||
_loadSessions() {
|
||||
try {
|
||||
if (!fs.existsSync(this.sessionsPath)) {
|
||||
return {};
|
||||
}
|
||||
const content = fs.readFileSync(this.sessionsPath, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.warn(`[!] Failed to load sessions: ${error.message}`);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save sessions to disk
|
||||
* @param {Object} sessions - Sessions object
|
||||
* @private
|
||||
*/
|
||||
_saveSessions(sessions) {
|
||||
try {
|
||||
const dir = path.dirname(this.sessionsPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
fs.writeFileSync(
|
||||
this.sessionsPath,
|
||||
JSON.stringify(sessions, null, 2),
|
||||
{ mode: 0o600 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`[!] Failed to save sessions: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { SessionManager };
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Parses Claude Code settings for tool restrictions
|
||||
*/
|
||||
class SettingsParser {
|
||||
/**
|
||||
* Parse default permission mode from project settings
|
||||
* @param {string} projectDir - Project directory (usually cwd)
|
||||
* @returns {string} Default permission mode (e.g., 'acceptEdits', 'bypassPermissions', 'plan', 'default')
|
||||
*/
|
||||
static parseDefaultPermissionMode(projectDir) {
|
||||
const settings = this._loadSettings(projectDir);
|
||||
const permissions = settings.permissions || {};
|
||||
|
||||
// Priority: local > shared > fallback to 'acceptEdits'
|
||||
const defaultMode = permissions.defaultMode || 'acceptEdits';
|
||||
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[i] Permission mode from settings: ${defaultMode}`);
|
||||
}
|
||||
|
||||
return defaultMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse project settings for tool restrictions
|
||||
* @param {string} projectDir - Project directory (usually cwd)
|
||||
* @returns {Object} { allowedTools: string[], disallowedTools: string[] }
|
||||
*/
|
||||
static parseToolRestrictions(projectDir) {
|
||||
const settings = this._loadSettings(projectDir);
|
||||
const permissions = settings.permissions || {};
|
||||
|
||||
const allowed = permissions.allow || [];
|
||||
const denied = permissions.deny || [];
|
||||
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[i] Tool restrictions: ${allowed.length} allowed, ${denied.length} denied`);
|
||||
}
|
||||
|
||||
return {
|
||||
allowedTools: allowed,
|
||||
disallowedTools: denied
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and merge settings files (local overrides shared)
|
||||
* @param {string} projectDir - Project directory
|
||||
* @returns {Object} Merged settings
|
||||
* @private
|
||||
*/
|
||||
static _loadSettings(projectDir) {
|
||||
const claudeDir = path.join(projectDir, '.claude');
|
||||
const sharedPath = path.join(claudeDir, 'settings.json');
|
||||
const localPath = path.join(claudeDir, 'settings.local.json');
|
||||
|
||||
// Load shared settings
|
||||
const shared = this._readJsonSafe(sharedPath) || {};
|
||||
|
||||
// Load local settings (overrides shared)
|
||||
const local = this._readJsonSafe(localPath) || {};
|
||||
|
||||
// Merge permissions (local overrides shared)
|
||||
return {
|
||||
permissions: {
|
||||
allow: [
|
||||
...(shared.permissions?.allow || []),
|
||||
...(local.permissions?.allow || [])
|
||||
],
|
||||
deny: [
|
||||
...(shared.permissions?.deny || []),
|
||||
...(local.permissions?.deny || [])
|
||||
],
|
||||
// Local defaultMode takes priority over shared
|
||||
defaultMode: local.permissions?.defaultMode || shared.permissions?.defaultMode || null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read JSON file safely (no throw)
|
||||
* @param {string} filePath - Path to JSON file
|
||||
* @returns {Object|null} Parsed JSON or null
|
||||
* @private
|
||||
*/
|
||||
static _readJsonSafe(filePath) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.warn(`[!] Failed to read settings: ${filePath}: ${error.message}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { SettingsParser };
|
||||
@@ -61,7 +61,9 @@ class Doctor {
|
||||
this.checkClaudeSettings();
|
||||
this.checkProfiles();
|
||||
this.checkInstances();
|
||||
this.checkDelegation();
|
||||
this.checkPermissions();
|
||||
this.checkCcsSymlinks();
|
||||
|
||||
this.showReport();
|
||||
return this.results;
|
||||
@@ -269,7 +271,64 @@ class Doctor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 7: File permissions
|
||||
* Check 7: Delegation system
|
||||
*/
|
||||
checkDelegation() {
|
||||
process.stdout.write('[?] Checking delegation... ');
|
||||
|
||||
// Check if delegation-rules.json exists
|
||||
const delegationRulesPath = path.join(this.ccsDir, 'delegation-rules.json');
|
||||
const hasDelegationRules = fs.existsSync(delegationRulesPath);
|
||||
|
||||
// Check if delegation commands exist
|
||||
const sharedCommandsDir = path.join(this.ccsDir, 'shared', 'commands', 'ccs');
|
||||
const hasGlmCommand = fs.existsSync(path.join(sharedCommandsDir, 'glm.md'));
|
||||
const hasKimiCommand = fs.existsSync(path.join(sharedCommandsDir, 'kimi.md'));
|
||||
const hasCreateCommand = fs.existsSync(path.join(sharedCommandsDir, 'create.md'));
|
||||
|
||||
if (!hasGlmCommand || !hasKimiCommand || !hasCreateCommand) {
|
||||
console.log(colored('[!]', 'yellow'), '(not installed)');
|
||||
this.results.addCheck(
|
||||
'Delegation',
|
||||
'warning',
|
||||
'Delegation commands not found',
|
||||
'Install with: npm install -g @kaitranntt/ccs --force'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check profile validity using DelegationValidator
|
||||
const { DelegationValidator } = require('../utils/delegation-validator');
|
||||
const readyProfiles = [];
|
||||
|
||||
for (const profile of ['glm', 'kimi']) {
|
||||
const validation = DelegationValidator.validate(profile);
|
||||
if (validation.valid) {
|
||||
readyProfiles.push(profile);
|
||||
}
|
||||
}
|
||||
|
||||
if (readyProfiles.length === 0) {
|
||||
console.log(colored('[!]', 'yellow'), '(no profiles ready)');
|
||||
this.results.addCheck(
|
||||
'Delegation',
|
||||
'warning',
|
||||
'Delegation installed but no profiles configured',
|
||||
'Configure profiles with valid API keys (not placeholders)'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(colored('[OK]', 'green'), `(${readyProfiles.join(', ')} ready)`);
|
||||
this.results.addCheck(
|
||||
'Delegation',
|
||||
'success',
|
||||
`${readyProfiles.length} profile(s) ready: ${readyProfiles.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 8: File permissions
|
||||
*/
|
||||
checkPermissions() {
|
||||
process.stdout.write('[?] Checking permissions... ');
|
||||
@@ -292,6 +351,40 @@ class Doctor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 9: CCS symlinks to ~/.claude/
|
||||
*/
|
||||
checkCcsSymlinks() {
|
||||
process.stdout.write('[?] Checking CCS symlinks... ');
|
||||
|
||||
try {
|
||||
const ClaudeSymlinkManager = require('../utils/claude-symlink-manager');
|
||||
const manager = new ClaudeSymlinkManager();
|
||||
const health = manager.checkHealth();
|
||||
|
||||
if (health.healthy) {
|
||||
console.log(colored('[OK]', 'green'));
|
||||
this.results.addCheck('CCS Symlinks', 'success', 'All CCS items properly symlinked');
|
||||
} else {
|
||||
console.log(colored('[!]', 'yellow'));
|
||||
this.results.addCheck(
|
||||
'CCS Symlinks',
|
||||
'warning',
|
||||
health.issues.join(', '),
|
||||
'Run: ccs update'
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(colored('[!]', 'yellow'));
|
||||
this.results.addCheck(
|
||||
'CCS Symlinks',
|
||||
'warning',
|
||||
'Could not check CCS symlinks: ' + e.message,
|
||||
'Run: ccs update'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show health check report
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* ClaudeSymlinkManager - Manages selective symlinks from ~/.ccs/.claude/ to ~/.claude/
|
||||
* v4.1.0: Selective symlinking for CCS items
|
||||
*
|
||||
* Purpose: Ship CCS items (.claude/) with package and symlink them to user's ~/.claude/
|
||||
* Architecture:
|
||||
* - ~/.ccs/.claude/* (source, ships with CCS)
|
||||
* - ~/.claude/* (target, gets selective symlinks)
|
||||
* - ~/.ccs/shared/ (UNTOUCHED, existing profile mechanism)
|
||||
*
|
||||
* Symlink Chain:
|
||||
* profile -> ~/.ccs/shared/ -> ~/.claude/ (which has symlinks to ~/.ccs/.claude/)
|
||||
*/
|
||||
class ClaudeSymlinkManager {
|
||||
constructor() {
|
||||
this.homeDir = os.homedir();
|
||||
this.ccsClaudeDir = path.join(this.homeDir, '.ccs', '.claude');
|
||||
this.userClaudeDir = path.join(this.homeDir, '.claude');
|
||||
|
||||
// CCS items to symlink (selective, item-level)
|
||||
this.ccsItems = [
|
||||
{ source: 'commands/ccs', target: 'commands/ccs', type: 'directory' },
|
||||
{ source: 'skills/ccs-delegation', target: 'skills/ccs-delegation', type: 'directory' },
|
||||
{ source: 'agents/ccs-delegator.md', target: 'agents/ccs-delegator.md', type: 'file' }
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Install CCS items to user's ~/.claude/ via selective symlinks
|
||||
* Safe: backs up existing files before creating symlinks
|
||||
*/
|
||||
install() {
|
||||
// Ensure ~/.ccs/.claude/ exists (should be shipped with package)
|
||||
if (!fs.existsSync(this.ccsClaudeDir)) {
|
||||
console.log('[!] CCS .claude/ directory not found, skipping symlink installation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create ~/.claude/ if missing
|
||||
if (!fs.existsSync(this.userClaudeDir)) {
|
||||
console.log('[i] Creating ~/.claude/ directory');
|
||||
fs.mkdirSync(this.userClaudeDir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
|
||||
// Install each CCS item
|
||||
for (const item of this.ccsItems) {
|
||||
this._installItem(item);
|
||||
}
|
||||
|
||||
console.log('[OK] CCS items installed to ~/.claude/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a single CCS item with conflict handling
|
||||
* @param {Object} item - Item descriptor {source, target, type}
|
||||
* @private
|
||||
*/
|
||||
_installItem(item) {
|
||||
const sourcePath = path.join(this.ccsClaudeDir, item.source);
|
||||
const targetPath = path.join(this.userClaudeDir, item.target);
|
||||
const targetDir = path.dirname(targetPath);
|
||||
|
||||
// Ensure source exists
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
console.log(`[!] Source not found: ${item.source}, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create target parent directory if needed
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
|
||||
// Check if target already exists
|
||||
if (fs.existsSync(targetPath)) {
|
||||
// Check if it's already the correct symlink
|
||||
if (this._isOurSymlink(targetPath, sourcePath)) {
|
||||
return; // Already correct, skip
|
||||
}
|
||||
|
||||
// Backup existing file/directory
|
||||
this._backupItem(targetPath);
|
||||
}
|
||||
|
||||
// Create symlink
|
||||
try {
|
||||
const symlinkType = item.type === 'directory' ? 'dir' : 'file';
|
||||
fs.symlinkSync(sourcePath, targetPath, symlinkType);
|
||||
console.log(`[OK] Symlinked ${item.target}`);
|
||||
} catch (err) {
|
||||
// Windows fallback: stub for now, full implementation in v4.2
|
||||
if (process.platform === 'win32') {
|
||||
console.log(`[!] Symlink failed for ${item.target} (Windows fallback deferred to v4.2)`);
|
||||
console.log(`[i] Enable Developer Mode or wait for next update`);
|
||||
} else {
|
||||
console.log(`[!] Failed to symlink ${item.target}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if target is already the correct symlink pointing to source
|
||||
* @param {string} targetPath - Target path to check
|
||||
* @param {string} expectedSource - Expected source path
|
||||
* @returns {boolean} True if target is correct symlink
|
||||
* @private
|
||||
*/
|
||||
_isOurSymlink(targetPath, expectedSource) {
|
||||
try {
|
||||
const stats = fs.lstatSync(targetPath);
|
||||
|
||||
if (!stats.isSymbolicLink()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const actualTarget = fs.readlinkSync(targetPath);
|
||||
const resolvedTarget = path.resolve(path.dirname(targetPath), actualTarget);
|
||||
|
||||
return resolvedTarget === expectedSource;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup existing item before replacing with symlink
|
||||
* @param {string} itemPath - Path to item to backup
|
||||
* @private
|
||||
*/
|
||||
_backupItem(itemPath) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
|
||||
const backupPath = `${itemPath}.backup-${timestamp}`;
|
||||
|
||||
try {
|
||||
// If backup already exists, use counter
|
||||
let finalBackupPath = backupPath;
|
||||
let counter = 1;
|
||||
while (fs.existsSync(finalBackupPath)) {
|
||||
finalBackupPath = `${backupPath}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
fs.renameSync(itemPath, finalBackupPath);
|
||||
console.log(`[i] Backed up existing item to ${path.basename(finalBackupPath)}`);
|
||||
} catch (err) {
|
||||
console.log(`[!] Failed to backup ${itemPath}: ${err.message}`);
|
||||
throw err; // Don't proceed if backup fails
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall CCS items from ~/.claude/ (remove symlinks only)
|
||||
* Safe: only removes items that are CCS symlinks
|
||||
*/
|
||||
uninstall() {
|
||||
let removed = 0;
|
||||
|
||||
for (const item of this.ccsItems) {
|
||||
const targetPath = path.join(this.userClaudeDir, item.target);
|
||||
const sourcePath = path.join(this.ccsClaudeDir, item.source);
|
||||
|
||||
// Only remove if it's our symlink
|
||||
if (fs.existsSync(targetPath) && this._isOurSymlink(targetPath, sourcePath)) {
|
||||
try {
|
||||
fs.unlinkSync(targetPath);
|
||||
console.log(`[OK] Removed ${item.target}`);
|
||||
removed++;
|
||||
} catch (err) {
|
||||
console.log(`[!] Failed to remove ${item.target}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`[OK] Removed ${removed} CCS items from ~/.claude/`);
|
||||
} else {
|
||||
console.log('[i] No CCS items to remove');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check symlink health and report issues
|
||||
* Used by 'ccs doctor' command
|
||||
* @returns {Object} Health check results {healthy: boolean, issues: string[]}
|
||||
*/
|
||||
checkHealth() {
|
||||
const issues = [];
|
||||
let healthy = true;
|
||||
|
||||
// Check if ~/.ccs/.claude/ exists
|
||||
if (!fs.existsSync(this.ccsClaudeDir)) {
|
||||
issues.push('CCS .claude/ directory missing (reinstall CCS)');
|
||||
healthy = false;
|
||||
return { healthy, issues };
|
||||
}
|
||||
|
||||
// Check each item
|
||||
for (const item of this.ccsItems) {
|
||||
const sourcePath = path.join(this.ccsClaudeDir, item.source);
|
||||
const targetPath = path.join(this.userClaudeDir, item.target);
|
||||
|
||||
// Check source exists
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
issues.push(`Source missing: ${item.source}`);
|
||||
healthy = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check target
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
issues.push(`Not installed: ${item.target} (run 'ccs update' to install)`);
|
||||
healthy = false;
|
||||
} else if (!this._isOurSymlink(targetPath, sourcePath)) {
|
||||
issues.push(`Not a CCS symlink: ${item.target} (run 'ccs update' to fix)`);
|
||||
healthy = false;
|
||||
}
|
||||
}
|
||||
|
||||
return { healthy, issues };
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-install symlinks (used by 'ccs update' command)
|
||||
* Same as install() but with explicit re-installation message
|
||||
*/
|
||||
update() {
|
||||
console.log('[i] Updating CCS items in ~/.claude/...');
|
||||
this.install();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClaudeSymlinkManager;
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* Validates delegation profiles for CCS delegation system
|
||||
* Ensures profiles exist and have valid API keys configured
|
||||
*/
|
||||
class DelegationValidator {
|
||||
/**
|
||||
* Validate a delegation profile
|
||||
* @param {string} profileName - Name of profile to validate (e.g., 'glm', 'kimi')
|
||||
* @returns {Object} Validation result { valid: boolean, error?: string, settingsPath?: string }
|
||||
*/
|
||||
static validate(profileName) {
|
||||
const homeDir = os.homedir();
|
||||
const settingsPath = path.join(homeDir, '.ccs', `${profileName}.settings.json`);
|
||||
|
||||
// Check if profile directory exists
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Profile not found: ${profileName}`,
|
||||
suggestion: `Profile settings missing at: ${settingsPath}\n\n` +
|
||||
`To set up ${profileName} profile:\n` +
|
||||
` 1. Copy base settings: cp config/base-${profileName}.settings.json ~/.ccs/${profileName}.settings.json\n` +
|
||||
` 2. Edit settings: Edit ~/.ccs/${profileName}.settings.json\n` +
|
||||
` 3. Set your API key in ANTHROPIC_AUTH_TOKEN field`
|
||||
};
|
||||
}
|
||||
|
||||
// Read and parse settings.json
|
||||
let settings;
|
||||
try {
|
||||
const settingsContent = fs.readFileSync(settingsPath, 'utf8');
|
||||
settings = JSON.parse(settingsContent);
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Failed to parse settings.json for ${profileName}`,
|
||||
suggestion: `Settings file is corrupted or invalid JSON.\n\n` +
|
||||
`Location: ${settingsPath}\n` +
|
||||
`Parse error: ${error.message}\n\n` +
|
||||
`Fix: Restore from base config:\n` +
|
||||
` cp config/base-${profileName}.settings.json ~/.ccs/${profileName}.settings.json`
|
||||
};
|
||||
}
|
||||
|
||||
// Validate API key exists and is not default
|
||||
const apiKey = settings.env?.ANTHROPIC_AUTH_TOKEN;
|
||||
|
||||
if (!apiKey) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `API key not configured for ${profileName}`,
|
||||
suggestion: `Missing ANTHROPIC_AUTH_TOKEN in settings.\n\n` +
|
||||
`Edit: ${settingsPath}\n` +
|
||||
`Set: env.ANTHROPIC_AUTH_TOKEN to your API key`
|
||||
};
|
||||
}
|
||||
|
||||
// Check for default placeholder values
|
||||
const defaultPlaceholders = [
|
||||
'YOUR_GLM_API_KEY_HERE',
|
||||
'YOUR_KIMI_API_KEY_HERE',
|
||||
'YOUR_API_KEY_HERE',
|
||||
'your-api-key-here',
|
||||
'PLACEHOLDER'
|
||||
];
|
||||
|
||||
if (defaultPlaceholders.some(placeholder => apiKey.includes(placeholder))) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Default API key placeholder detected for ${profileName}`,
|
||||
suggestion: `API key is still set to default placeholder.\n\n` +
|
||||
`To configure your profile:\n` +
|
||||
` 1. Edit: ${settingsPath}\n` +
|
||||
` 2. Replace ANTHROPIC_AUTH_TOKEN with your actual API key\n\n` +
|
||||
`Get API key:\n` +
|
||||
` GLM: https://z.ai/manage-apikey/apikey-list\n` +
|
||||
` Kimi: https://platform.moonshot.cn/console/api-keys`
|
||||
};
|
||||
}
|
||||
|
||||
// Validation passed
|
||||
return {
|
||||
valid: true,
|
||||
settingsPath,
|
||||
apiKey: apiKey.substring(0, 8) + '...' // Show first 8 chars for verification
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format validation error for display
|
||||
* @param {Object} result - Validation result from validate()
|
||||
* @returns {string} Formatted error message
|
||||
*/
|
||||
static formatError(result) {
|
||||
if (result.valid) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let message = `\n[X] ${result.error}\n\n`;
|
||||
|
||||
if (result.suggestion) {
|
||||
message += `${result.suggestion}\n`;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if profile is delegation-ready (shorthand)
|
||||
* @param {string} profileName - Profile to check
|
||||
* @returns {boolean} True if ready for delegation
|
||||
*/
|
||||
static isReady(profileName) {
|
||||
const result = this.validate(profileName);
|
||||
return result.valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all delegation-ready profiles
|
||||
* @returns {Array<string>} List of profile names ready for delegation
|
||||
*/
|
||||
static getReadyProfiles() {
|
||||
const homeDir = os.homedir();
|
||||
const ccsDir = path.join(homeDir, '.ccs');
|
||||
|
||||
if (!fs.existsSync(ccsDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const profiles = [];
|
||||
const entries = fs.readdirSync(ccsDir, { withFileTypes: true });
|
||||
|
||||
// Look for *.settings.json files
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && entry.name.endsWith('.settings.json')) {
|
||||
const profileName = entry.name.replace('.settings.json', '');
|
||||
if (this.isReady(profileName)) {
|
||||
profiles.push(profileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { DelegationValidator };
|
||||
@@ -0,0 +1,492 @@
|
||||
# CCS Delegation Workflow Diagrams
|
||||
|
||||
Visual guide to understanding how CCS delegation works internally.
|
||||
|
||||
---
|
||||
|
||||
## Overview Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CCS Architecture │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ User Input │
|
||||
│ │ │
|
||||
│ ├─── ccs glm → Normal Profile Execution │
|
||||
│ │ │
|
||||
│ └─── ccs glm -p "task" → Delegation Flow ⚡ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Execution Flow Comparison
|
||||
|
||||
### Normal Execution (Without -p)
|
||||
```
|
||||
User: ccs glm
|
||||
│
|
||||
├─→ bin/ccs.js (main entry)
|
||||
│ │
|
||||
│ ├─→ Profile Detection: "glm"
|
||||
│ │
|
||||
│ └─→ execClaude()
|
||||
│ │
|
||||
│ └─→ spawn("claude", ["--settings", "~/.ccs/glm.settings"])
|
||||
│ │
|
||||
│ └─→ Claude CLI Interactive Session
|
||||
│ │
|
||||
│ └─→ Direct User Interaction
|
||||
```
|
||||
|
||||
### Delegation Execution (With -p)
|
||||
```
|
||||
User: ccs glm -p "add tests for UserService"
|
||||
│
|
||||
├─→ bin/ccs.js (main entry)
|
||||
│ │
|
||||
│ ├─→ -p Flag Detected! 🎯
|
||||
│ │
|
||||
│ └─→ DelegationHandler.route(args)
|
||||
│ │
|
||||
│ ├─→ Parse args
|
||||
│ │ ├─ profile: "glm"
|
||||
│ │ ├─ prompt: "add tests for UserService"
|
||||
│ │ └─ options: { outputFormat: "stream-json", timeout: 600000 }
|
||||
│ │
|
||||
│ ├─→ Validate profile (DelegationValidator)
|
||||
│ │
|
||||
│ └─→ HeadlessExecutor.execute("glm", prompt, options)
|
||||
│ │
|
||||
│ ├─→ spawn("claude", [
|
||||
│ │ "-p", prompt,
|
||||
│ │ "--settings", "~/.ccs/glm.settings",
|
||||
│ │ "--output-format", "stream-json",
|
||||
│ │ "--permission-mode", "acceptEdits"
|
||||
│ │ ])
|
||||
│ │
|
||||
│ ├─→ Parse stream-JSON output (jsonl format)
|
||||
│ │ {"type":"init","session_id":"abc123"}
|
||||
│ │ {"type":"assistant","message":{...}}
|
||||
│ │ {"type":"result","total_cost_usd":0.0042,"num_turns":3}
|
||||
│ │
|
||||
│ ├─→ SessionManager.saveSession()
|
||||
│ │ └─→ ~/.ccs/delegation-sessions.json
|
||||
│ │
|
||||
│ └─→ ResultFormatter.format(result)
|
||||
│ │
|
||||
│ └─→ ASCII Box Output
|
||||
│ ╔════════════════════════╗
|
||||
│ ║ Session: abc123 ║
|
||||
│ ║ Cost: $0.0042 ║
|
||||
│ ║ Turns: 3 ║
|
||||
│ ╚════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Continue Command Flow
|
||||
|
||||
### Multi-Turn Session Workflow
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Turn 1: Initial Task │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
User: ccs glm -p "implement user registration"
|
||||
│
|
||||
└─→ HeadlessExecutor
|
||||
│
|
||||
├─→ Execute with fresh session
|
||||
│
|
||||
└─→ Save session metadata:
|
||||
{
|
||||
"profile": "glm",
|
||||
"sessionId": "session-001",
|
||||
"totalCost": 0.0025,
|
||||
"turns": 2,
|
||||
"cwd": "/path/to/project",
|
||||
"lastUpdated": "2025-11-15T18:00:00Z"
|
||||
}
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Turn 2: Continue Session │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
User: ccs glm:continue -p "add validation tests"
|
||||
│
|
||||
└─→ DelegationHandler detects ":continue" suffix
|
||||
│
|
||||
├─→ Extract base profile: "glm"
|
||||
│
|
||||
├─→ SessionManager.getLastSession("glm")
|
||||
│ └─→ Returns: { sessionId: "session-001", ... }
|
||||
│
|
||||
└─→ HeadlessExecutor.execute("glm", prompt, {
|
||||
resumeSession: true,
|
||||
sessionId: "session-001" ← Resume!
|
||||
})
|
||||
│
|
||||
├─→ spawn("claude", [
|
||||
│ "-p", "add validation tests",
|
||||
│ "--resume", "session-001", ← Continue!
|
||||
│ "--output-format", "stream-json",
|
||||
│ "--verbose",
|
||||
│ ...
|
||||
│ ])
|
||||
│
|
||||
└─→ Update session metadata:
|
||||
{
|
||||
"sessionId": "session-001", ← Same session
|
||||
"totalCost": 0.0067, ← Aggregated
|
||||
"turns": 5, ← Incremented
|
||||
"lastUpdated": "2025-11-15T18:05:00Z"
|
||||
}
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Turn 3+: Multiple Continues │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
User: ccs glm:continue -p "run the tests"
|
||||
│
|
||||
└─→ Same flow, cost keeps aggregating:
|
||||
{
|
||||
"totalCost": 0.0089, ← $0.0025 + $0.0042 + $0.0022
|
||||
"turns": 7 ← 2 + 3 + 2
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Management Architecture
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ ~/.ccs/delegation-sessions.json │
|
||||
├───────────────────────────────────────────────────────────────┤
|
||||
│ { │
|
||||
│ "glm": { │
|
||||
│ "sessionId": "abc123-def456", │
|
||||
│ "totalCost": 0.0067, ← Aggregated across turns │
|
||||
│ "turns": 5, ← Total turn count │
|
||||
│ "cwd": "/home/user/project", ← Working directory │
|
||||
│ "lastUpdated": "2025-11-15T18:05:00Z", │
|
||||
│ "expiresAt": "2025-12-15T18:05:00Z" ← 30 days │
|
||||
│ }, │
|
||||
│ "kimi": { │
|
||||
│ "sessionId": "xyz789-uvw012", │
|
||||
│ "totalCost": 0.0123, │
|
||||
│ "turns": 8, │
|
||||
│ "cwd": "/home/user/other-project", │
|
||||
│ "lastUpdated": "2025-11-14T10:30:00Z", │
|
||||
│ "expiresAt": "2025-12-14T10:30:00Z" │
|
||||
│ } │
|
||||
│ } │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
|
||||
Operations:
|
||||
├─→ saveSession(profile, metadata) → Write to file
|
||||
├─→ getLastSession(profile) → Read from file
|
||||
├─→ updateSession(profile, updates) → Merge + write
|
||||
└─→ cleanupExpired() → Remove old sessions (>30 days)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decision Flow: When to Delegate
|
||||
|
||||
```
|
||||
User Task Request
|
||||
│
|
||||
├─→ Task Analysis (ccs-delegator agent)
|
||||
│ │
|
||||
│ ├─→ Read ccs-delegation skill
|
||||
│ │
|
||||
│ └─→ Pattern Matching:
|
||||
│ │
|
||||
│ ├─→ Match delegation patterns?
|
||||
│ │ ├─ "refactor .* to use async/await" ✓
|
||||
│ │ ├─ "add tests for .*" ✓
|
||||
│ │ ├─ "fix typos in .*" ✓
|
||||
│ │ └─ ...
|
||||
│ │
|
||||
│ ├─→ Match anti-patterns?
|
||||
│ │ ├─ "implement .*" ✗
|
||||
│ │ ├─ "optimize .*" ✗
|
||||
│ │ └─ "design .*" ✗
|
||||
│ │
|
||||
│ └─→ Check criteria:
|
||||
│ ├─ Scope: < 5 files? ✓
|
||||
│ ├─ Complexity: Mechanical? ✓
|
||||
│ ├─ Ambiguity: Zero decisions? ✓
|
||||
│ └─ Context: Patterns exist? ✓
|
||||
│
|
||||
└─→ Decision:
|
||||
│
|
||||
├─→ YES → Delegate
|
||||
│ └─→ ccs glm -p "task"
|
||||
│
|
||||
└─→ NO → Keep in main session
|
||||
└─→ Handle directly in conversation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cost Tracking & Token Optimization
|
||||
|
||||
### Traditional Main Session Flow
|
||||
```
|
||||
User: "Add tests for UserService, AuthService, and OrderService"
|
||||
│
|
||||
└─→ Claude in main session:
|
||||
│
|
||||
├─→ Loads full context (2000+ tokens)
|
||||
├─→ Discusses approach with user
|
||||
├─→ Implements UserService tests
|
||||
├─→ Shows code, waits for approval
|
||||
├─→ Implements AuthService tests
|
||||
├─→ Shows code, waits for approval
|
||||
├─→ Implements OrderService tests
|
||||
└─→ Total: ~8000 tokens, $0.032
|
||||
|
||||
Main Session Cost:
|
||||
Context load: 2000 tokens
|
||||
Discussion: 1500 tokens
|
||||
Implementation: 4500 tokens
|
||||
────────────────────────────
|
||||
Total: 8000 tokens → $0.032
|
||||
```
|
||||
|
||||
### Delegation Flow (Token Optimized)
|
||||
```
|
||||
User: "Add tests for UserService, AuthService, and OrderService"
|
||||
│
|
||||
└─→ ccs-delegator agent:
|
||||
│
|
||||
├─→ Analyzes: 3 similar tasks → Batch delegate
|
||||
│
|
||||
├─→ Execute 3 delegations:
|
||||
│ │
|
||||
│ ├─→ ccs glm -p "add tests for UserService"
|
||||
│ │ └─→ Cost: $0.0015 (500 tokens)
|
||||
│ │
|
||||
│ ├─→ ccs glm -p "add tests for AuthService"
|
||||
│ │ └─→ Cost: $0.0015 (500 tokens)
|
||||
│ │
|
||||
│ └─→ ccs glm -p "add tests for OrderService"
|
||||
│ └─→ Cost: $0.0015 (500 tokens)
|
||||
│
|
||||
└─→ Total: ~1500 tokens, $0.0045
|
||||
|
||||
Delegation Cost:
|
||||
Task 1 (GLM): 500 tokens → $0.0015
|
||||
Task 2 (GLM): 500 tokens → $0.0015
|
||||
Task 3 (GLM): 500 tokens → $0.0015
|
||||
────────────────────────────────────────
|
||||
Total: 1500 tokens → $0.0045
|
||||
|
||||
Savings: $0.032 - $0.0045 = $0.0275 (86% reduction) ⚡
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Points Summary
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Integration Points │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. bin/ccs.js (lines 501-507) │
|
||||
│ └─→ Detects -p flag → Routes to DelegationHandler │
|
||||
│ │
|
||||
│ 2. bin/delegation/delegation-handler.js (NEW) │
|
||||
│ └─→ Orchestrates delegation flow │
|
||||
│ │
|
||||
│ 3. bin/delegation/headless-executor.js (EXISTING) │
|
||||
│ └─→ Spawns claude -p with enhanced flags │
|
||||
│ │
|
||||
│ 4. bin/delegation/session-manager.js (EXISTING) │
|
||||
│ └─→ Persists session metadata │
|
||||
│ │
|
||||
│ 5. bin/delegation/result-formatter.js (EXISTING) │
|
||||
│ └─→ Formats ASCII box output │
|
||||
│ │
|
||||
│ 6. .claude/commands/ccs/glm.md │
|
||||
│ └─→ Executes: ccs glm -p "$ARGUMENTS" │
|
||||
│ │
|
||||
│ 7. .claude/agents/ccs-delegator.md │
|
||||
│ └─→ Proactive delegation via Task tool │
|
||||
│ │
|
||||
│ 8. .claude/skills/ccs-delegation/ │
|
||||
│ └─→ AI decision framework + technical docs │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Effectiveness Metrics
|
||||
|
||||
### Feature Coverage
|
||||
|
||||
```
|
||||
✅ Stream-JSON Output Parsing
|
||||
└─→ Real-time jsonl format, extracts: session_id, cost, turns, errors
|
||||
|
||||
✅ Real-Time Tool Visibility
|
||||
└─→ Shows: [Tool] Bash: npm install, [Tool] Write: index.html
|
||||
|
||||
✅ Session Management
|
||||
└─→ Persists to: ~/.ccs/delegation-sessions.json
|
||||
|
||||
✅ Multi-Turn Support
|
||||
└─→ Resume via: ccs glm:continue -p "task"
|
||||
|
||||
✅ Cost Tracking
|
||||
└─→ Aggregates across turns, displays in USD
|
||||
|
||||
✅ Time-Based Limits
|
||||
└─→ Default: 10min timeout with graceful SIGTERM termination
|
||||
|
||||
✅ Permission Mode
|
||||
└─→ Default: acceptEdits (auto-approve file ops)
|
||||
|
||||
✅ Signal Handling
|
||||
└─→ Kills child process on Ctrl+C/Esc (no orphans)
|
||||
|
||||
✅ Slash Command Preservation
|
||||
└─→ Detects /cook, /plan in prompts, keeps at start
|
||||
|
||||
✅ Formatted Output
|
||||
└─→ ASCII box with metadata
|
||||
```
|
||||
|
||||
### Performance Impact
|
||||
|
||||
```
|
||||
Metric Before After Improvement
|
||||
────────────────────────────────────────────────────────────
|
||||
Session overhead 2000 tok 500 tok 75% ↓
|
||||
Cost per simple task $0.008 $0.0015 81% ↓
|
||||
Time to result ~30s ~10s 67% ↓
|
||||
Context pollution High Zero 100% ↓
|
||||
Batch 3 tasks $0.024 $0.0045 81% ↓
|
||||
```
|
||||
|
||||
### User Experience Flow
|
||||
|
||||
```
|
||||
BEFORE (Manual):
|
||||
User: "Add tests for UserService"
|
||||
Claude: "I'll add tests for UserService..."
|
||||
[Generates code in main session, uses context]
|
||||
Claude: "Here are the tests..."
|
||||
User: "Now add tests for AuthService"
|
||||
Claude: "I'll add tests for AuthService..."
|
||||
[Repeats, accumulates context]
|
||||
|
||||
AFTER (Delegated):
|
||||
User: "Add tests for UserService, AuthService, OrderService"
|
||||
Claude: "I'll delegate these similar tasks to GLM for token optimization"
|
||||
[Batch delegates via ccs-delegator agent]
|
||||
ccs glm -p "add tests for UserService" → $0.0015
|
||||
ccs glm -p "add tests for AuthService" → $0.0015
|
||||
ccs glm -p "add tests for OrderService" → $0.0015
|
||||
Claude: "All tests added. Total cost: $0.0045"
|
||||
[Main session context stays clean]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Benefits
|
||||
|
||||
### 1. Separation of Concerns
|
||||
```
|
||||
bin/ccs.js → Routing only (6 lines added)
|
||||
delegation-handler.js → Orchestration logic
|
||||
headless-executor.js → Execution engine
|
||||
session-manager.js → State persistence
|
||||
result-formatter.js → Output formatting
|
||||
```
|
||||
|
||||
### 2. Progressive Disclosure
|
||||
```
|
||||
SKILL.md → Entry point (56 lines)
|
||||
└─→ headless-workflow.md → Technical details (155 lines)
|
||||
└─→ delegation-guidelines.md → AI decision rules (100 lines)
|
||||
```
|
||||
|
||||
### 3. Zero Breaking Changes
|
||||
```
|
||||
ccs glm → Works as before (normal profile)
|
||||
ccs glm -p "task" → NEW: Enhanced delegation
|
||||
ccs glm:continue -p → NEW: Multi-turn support
|
||||
```
|
||||
|
||||
### 4. Token Efficiency
|
||||
```
|
||||
Main session: Full context loaded for every task
|
||||
Delegation: Isolated execution, no context pollution
|
||||
Savings: 81% cost reduction on simple tasks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Improvements
|
||||
```
|
||||
1. Cost Alerts
|
||||
└─→ Warn if delegation > $1.00
|
||||
|
||||
2. Session Analytics
|
||||
└─→ Track delegation patterns, identify high-cost tasks
|
||||
|
||||
3. Batch Optimization
|
||||
└─→ Auto-detect batchable tasks: "add tests for all *.service.js"
|
||||
|
||||
4. Profile Auto-Selection
|
||||
└─→ Agent chooses GLM vs Kimi based on file count
|
||||
|
||||
5. GLMT Integration
|
||||
└─→ Complex reasoning tasks via glmt proxy + delegation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Flows
|
||||
|
||||
### Common Issues
|
||||
|
||||
```
|
||||
Issue: "No previous session found for glm"
|
||||
│
|
||||
└─→ Cause: Using :continue without initial session
|
||||
│
|
||||
└─→ Solution: Run initial task first
|
||||
ccs glm -p "initial task"
|
||||
ccs glm:continue -p "follow up"
|
||||
|
||||
Issue: "Profile not configured for delegation"
|
||||
│
|
||||
└─→ Cause: Missing ~/.ccs/glm.settings.json
|
||||
│
|
||||
└─→ Solution: Run ccs doctor
|
||||
ccs doctor
|
||||
→ Shows configuration issues
|
||||
|
||||
Issue: "Missing prompt after -p flag"
|
||||
│
|
||||
└─→ Cause: No argument after -p
|
||||
│
|
||||
└─→ Solution: Provide prompt in quotes
|
||||
ccs glm -p "task description"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-16
|
||||
**Related**: `SKILL.md`, `headless-workflow.md`, `delegation-guidelines.md`
|
||||
+70
-1
@@ -31,7 +31,7 @@ $InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or (
|
||||
# IMPORTANT: Update this version when releasing new versions!
|
||||
# This hardcoded version is used for standalone installations (irm | iex)
|
||||
# For git installations, VERSION file is read if available
|
||||
$CcsVersion = "3.5.0"
|
||||
$CcsVersion = "4.1.0"
|
||||
|
||||
# Try to read VERSION file for git installations
|
||||
if ($ScriptDir) {
|
||||
@@ -621,6 +621,75 @@ Write-Host "[i] Setting up shared directories..."
|
||||
Initialize-SharedSymlinks
|
||||
Write-Host ""
|
||||
|
||||
# Install CCS items to ~/.claude/ via symlinks (v4.1.0)
|
||||
Write-Host "[i] Installing CCS items to ~/.claude/..."
|
||||
if (Get-Command node -ErrorAction SilentlyContinue) {
|
||||
# Check if .claude/ was successfully installed
|
||||
if (Test-Path "$CcsDir\.claude") {
|
||||
# Download or copy claude-symlink-manager.js
|
||||
$UtilsDir = "$CcsDir\bin\utils"
|
||||
if (-not (Test-Path $UtilsDir)) {
|
||||
New-Item -ItemType Directory -Path $UtilsDir -Force | Out-Null
|
||||
}
|
||||
|
||||
if ($InstallMethod -eq "git" -and $ScriptDir) {
|
||||
# Git install - copy from local repo
|
||||
$SourcePath = $null
|
||||
if (Test-Path "$ScriptDir\..\bin\utils\claude-symlink-manager.js") {
|
||||
$SourcePath = "$ScriptDir\..\bin\utils\claude-symlink-manager.js"
|
||||
} elseif (Test-Path "$ScriptDir\bin\utils\claude-symlink-manager.js") {
|
||||
$SourcePath = "$ScriptDir\bin\utils\claude-symlink-manager.js"
|
||||
}
|
||||
|
||||
if ($SourcePath) {
|
||||
Copy-Item $SourcePath "$UtilsDir\claude-symlink-manager.js" -Force
|
||||
}
|
||||
} else {
|
||||
# Standalone install - download from GitHub
|
||||
try {
|
||||
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/kaitranntt/ccs/main/bin/utils/claude-symlink-manager.js" `
|
||||
-OutFile "$UtilsDir\claude-symlink-manager.js" -UseBasicParsing
|
||||
} catch {
|
||||
Write-Host "[!] Failed to download claude-symlink-manager.js"
|
||||
}
|
||||
}
|
||||
|
||||
# Call ClaudeSymlinkManager if available
|
||||
if (Test-Path "$UtilsDir\claude-symlink-manager.js") {
|
||||
try {
|
||||
$scriptBlock = @"
|
||||
try {
|
||||
const ClaudeSymlinkManager = require('$($UtilsDir -replace '\\', '/')/claude-symlink-manager.js');
|
||||
const manager = new ClaudeSymlinkManager();
|
||||
manager.install();
|
||||
} catch (err) {
|
||||
console.log('[!] CCS item installation warning: ' + err.message);
|
||||
console.log(' Run "ccs update" to retry');
|
||||
}
|
||||
"@
|
||||
node -e $scriptBlock 2>$null
|
||||
if (-not $?) {
|
||||
Write-Host "[!] CCS item installation skipped (run 'ccs update' later)"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "[!] CCS item installation failed: $($_.Exception.Message)"
|
||||
Write-Host " Run 'ccs update' after installation to complete setup"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[!] claude-symlink-manager.js not found, skipping"
|
||||
Write-Host " Run 'ccs update' after installation to complete setup"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[!] .claude/ folder not found, skipping CCS item installation"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[!] Node.js not found, skipping CCS item installation"
|
||||
Write-Host " Install Node.js and run 'ccs update' to complete setup"
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host "[i] Note: Windows symlink support requires Developer Mode (v4.2 will add fallback)"
|
||||
Write-Host ""
|
||||
|
||||
# Check and update PATH
|
||||
$UserPath = [Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User)
|
||||
if ($UserPath -notlike "*$CcsDir*") {
|
||||
|
||||
+48
-1
@@ -32,7 +32,7 @@ fi
|
||||
# IMPORTANT: Update this version when releasing new versions!
|
||||
# This hardcoded version is used for standalone installations (curl | bash)
|
||||
# For git installations, VERSION file is read if available
|
||||
CCS_VERSION="3.5.0"
|
||||
CCS_VERSION="4.1.0"
|
||||
|
||||
# Try to read VERSION file for git installations
|
||||
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
|
||||
@@ -769,6 +769,53 @@ echo "[i] Setting up shared directories..."
|
||||
setup_shared_symlinks
|
||||
echo ""
|
||||
|
||||
# Install CCS items to ~/.claude/ via symlinks (v4.1.0)
|
||||
echo "[i] Installing CCS items to ~/.claude/..."
|
||||
if command -v node &> /dev/null; then
|
||||
# Check if .claude/ was successfully installed
|
||||
if [[ -d "$CCS_DIR/.claude" ]]; then
|
||||
# Download or copy claude-symlink-manager.js
|
||||
mkdir -p "$CCS_DIR/bin/utils"
|
||||
|
||||
if [[ "$INSTALL_METHOD" == "git" ]]; then
|
||||
# Git install - copy from local repo
|
||||
if [[ -f "$SCRIPT_DIR/../bin/utils/claude-symlink-manager.js" ]]; then
|
||||
cp "$SCRIPT_DIR/../bin/utils/claude-symlink-manager.js" "$CCS_DIR/bin/utils/claude-symlink-manager.js"
|
||||
elif [[ -f "$SCRIPT_DIR/bin/utils/claude-symlink-manager.js" ]]; then
|
||||
cp "$SCRIPT_DIR/bin/utils/claude-symlink-manager.js" "$CCS_DIR/bin/utils/claude-symlink-manager.js"
|
||||
fi
|
||||
else
|
||||
# Standalone install - download from GitHub
|
||||
if ! curl -fsSL "https://raw.githubusercontent.com/kaitranntt/ccs/main/bin/utils/claude-symlink-manager.js" -o "$CCS_DIR/bin/utils/claude-symlink-manager.js" 2>/dev/null; then
|
||||
echo "[!] Failed to download claude-symlink-manager.js"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Call ClaudeSymlinkManager if available
|
||||
if [[ -f "$CCS_DIR/bin/utils/claude-symlink-manager.js" ]]; then
|
||||
node -e "
|
||||
try {
|
||||
const ClaudeSymlinkManager = require('$CCS_DIR/bin/utils/claude-symlink-manager.js');
|
||||
const manager = new ClaudeSymlinkManager();
|
||||
manager.install();
|
||||
} catch (err) {
|
||||
console.log('[!] CCS item installation warning: ' + err.message);
|
||||
console.log(' Run \"ccs update\" to retry');
|
||||
}
|
||||
" 2>/dev/null || echo "[!] CCS item installation skipped (run 'ccs update' later)"
|
||||
else
|
||||
echo "[!] claude-symlink-manager.js not found, skipping"
|
||||
echo " Run 'ccs update' after installation to complete setup"
|
||||
fi
|
||||
else
|
||||
echo "[!] .claude/ folder not found, skipping CCS item installation"
|
||||
fi
|
||||
else
|
||||
echo "[!] Node.js not found, skipping CCS item installation"
|
||||
echo " Install Node.js and run 'ccs update' to complete setup"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Auto-configure PATH if needed (all Unix platforms)
|
||||
configure_shell_path
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
# Version (updated by scripts/bump-version.sh)
|
||||
CCS_VERSION="3.5.0"
|
||||
CCS_VERSION="4.1.0"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
|
||||
readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
|
||||
@@ -187,6 +187,13 @@ show_help() {
|
||||
echo -e " ${YELLOW}ccs work${RESET} Switch to work account"
|
||||
echo -e " ${YELLOW}ccs personal${RESET} Switch to personal account"
|
||||
echo ""
|
||||
echo -e "${CYAN}Delegation (Token Optimization):${RESET}"
|
||||
echo -e " ${YELLOW}/ccs:glm \"task\"${RESET} Delegate to GLM-4.6 within Claude session"
|
||||
echo -e " ${YELLOW}/ccs:kimi \"task\"${RESET} Delegate to Kimi for long context"
|
||||
echo -e " ${YELLOW}/ccs:create m2${RESET} Create custom delegation command"
|
||||
echo -e " Use delegation to save tokens on simple tasks"
|
||||
echo -e " Commands work inside Claude Code sessions only"
|
||||
echo ""
|
||||
echo -e "${CYAN}Diagnostics:${RESET}"
|
||||
echo -e " ${YELLOW}ccs doctor${RESET} Run health check and diagnostics"
|
||||
echo ""
|
||||
@@ -522,6 +529,33 @@ show_version() {
|
||||
# Simple config display
|
||||
local config="${CCS_CONFIG:-$HOME/.ccs/config.json}"
|
||||
echo -e " ${CYAN}Config:${RESET} ${config}"
|
||||
|
||||
# Delegation status
|
||||
local delegation_rules="$HOME/.ccs/delegation-rules.json"
|
||||
if [[ -f "$delegation_rules" ]]; then
|
||||
echo -e " ${CYAN}Delegation:${RESET} Enabled"
|
||||
|
||||
# Check which profiles are delegation-ready
|
||||
local ready_profiles=()
|
||||
for profile in glm kimi; do
|
||||
local settings_file="$HOME/.ccs/profiles/$profile/settings.json"
|
||||
if [[ -f "$settings_file" ]]; then
|
||||
# Check if API key is configured (not a placeholder)
|
||||
local api_key=$(jq -r '.env.ANTHROPIC_AUTH_TOKEN // empty' "$settings_file" 2>/dev/null)
|
||||
if [[ -n "$api_key" ]] && [[ ! "$api_key" =~ YOUR_.*_API_KEY_HERE ]]; then
|
||||
ready_profiles+=("$profile")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#ready_profiles[@]} -gt 0 ]]; then
|
||||
echo -e " ${CYAN}Ready:${RESET} ${ready_profiles[*]}"
|
||||
else
|
||||
echo -e " ${CYAN}Ready:${RESET} None (configure profiles first)"
|
||||
fi
|
||||
else
|
||||
echo -e " ${CYAN}Delegation:${RESET} Not configured"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}Documentation:${RESET} https://github.com/kaitranntt/ccs"
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Version (updated by scripts/bump-version.sh)
|
||||
$CcsVersion = "3.5.0"
|
||||
$CcsVersion = "4.1.0"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" }
|
||||
$ProfilesJson = "$env:USERPROFILE\.ccs\profiles.json"
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kaitranntt/ccs",
|
||||
"version": "3.5.0",
|
||||
"version": "4.1.0",
|
||||
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -31,6 +31,7 @@
|
||||
"lib/",
|
||||
"scripts/",
|
||||
"config/",
|
||||
".claude/",
|
||||
"VERSION",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
|
||||
@@ -103,6 +103,17 @@ function createConfigFiles() {
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Install CCS items to ~/.claude/ (v4.1.0)
|
||||
try {
|
||||
const ClaudeSymlinkManager = require('../bin/utils/claude-symlink-manager');
|
||||
const claudeSymlinkManager = new ClaudeSymlinkManager();
|
||||
claudeSymlinkManager.install();
|
||||
} catch (err) {
|
||||
console.warn('[!] CCS item installation warning:', err.message);
|
||||
console.warn(' Run "ccs update" to retry');
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Create config.json if missing
|
||||
const configPath = path.join(ccsDir, 'config.json');
|
||||
if (!fs.existsSync(configPath)) {
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const { HeadlessExecutor } = require('../../../bin/delegation/headless-executor');
|
||||
|
||||
/**
|
||||
* Test runner
|
||||
*/
|
||||
class TestRunner {
|
||||
constructor() {
|
||||
this.tests = [];
|
||||
this.passed = 0;
|
||||
this.failed = 0;
|
||||
}
|
||||
|
||||
test(name, fn) {
|
||||
this.tests.push({ name, fn });
|
||||
}
|
||||
|
||||
async run() {
|
||||
console.log('\n=== Permission Mode Tests ===\n');
|
||||
|
||||
for (const { name, fn } of this.tests) {
|
||||
try {
|
||||
await fn();
|
||||
console.log(`[OK] ${name}`);
|
||||
this.passed++;
|
||||
} catch (error) {
|
||||
console.error(`[X] ${name}`);
|
||||
console.error(` Error: ${error.message}`);
|
||||
this.failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nResults: ${this.passed} passed, ${this.failed} failed`);
|
||||
process.exit(this.failed > 0 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message || 'Assertion failed');
|
||||
}
|
||||
|
||||
// Test suite
|
||||
const runner = new TestRunner();
|
||||
|
||||
/**
|
||||
* Test 1: Validation accepts valid modes
|
||||
*/
|
||||
runner.test('Validate acceptEdits mode', () => {
|
||||
// Should not throw
|
||||
HeadlessExecutor._validatePermissionMode('acceptEdits');
|
||||
});
|
||||
|
||||
runner.test('Validate plan mode', () => {
|
||||
HeadlessExecutor._validatePermissionMode('plan');
|
||||
});
|
||||
|
||||
runner.test('Validate default mode', () => {
|
||||
HeadlessExecutor._validatePermissionMode('default');
|
||||
});
|
||||
|
||||
runner.test('Validate bypassPermissions mode', () => {
|
||||
HeadlessExecutor._validatePermissionMode('bypassPermissions');
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 2: Validation rejects invalid modes
|
||||
*/
|
||||
runner.test('Reject invalid mode', () => {
|
||||
let thrown = false;
|
||||
try {
|
||||
HeadlessExecutor._validatePermissionMode('invalidMode');
|
||||
} catch (error) {
|
||||
thrown = true;
|
||||
assert(error.message.includes('Invalid permission mode'), 'Error message should mention invalid mode');
|
||||
assert(error.message.includes('invalidMode'), 'Error should show the invalid value');
|
||||
}
|
||||
assert(thrown, 'Should throw error for invalid mode');
|
||||
});
|
||||
|
||||
runner.test('Reject empty mode', () => {
|
||||
let thrown = false;
|
||||
try {
|
||||
HeadlessExecutor._validatePermissionMode('');
|
||||
} catch (error) {
|
||||
thrown = true;
|
||||
}
|
||||
assert(thrown, 'Should throw error for empty mode');
|
||||
});
|
||||
|
||||
runner.test('Reject null mode', () => {
|
||||
let thrown = false;
|
||||
try {
|
||||
HeadlessExecutor._validatePermissionMode(null);
|
||||
} catch (error) {
|
||||
thrown = true;
|
||||
}
|
||||
assert(thrown, 'Should throw error for null mode');
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 3: CLI args construction (simulation)
|
||||
*/
|
||||
runner.test('Build args for acceptEdits mode', () => {
|
||||
const args = ['-p', 'test', '--settings', '/path/settings.json'];
|
||||
const permissionMode = 'acceptEdits';
|
||||
|
||||
if (permissionMode && permissionMode !== 'default') {
|
||||
if (permissionMode === 'bypassPermissions') {
|
||||
args.push('--dangerously-skip-permissions');
|
||||
} else {
|
||||
args.push('--permission-mode', permissionMode);
|
||||
}
|
||||
}
|
||||
|
||||
assert(args.includes('--permission-mode'), 'Should have permission-mode flag');
|
||||
assert(args.includes('acceptEdits'), 'Should have acceptEdits value');
|
||||
assert(!args.includes('--dangerously-skip-permissions'), 'Should not have bypass flag');
|
||||
});
|
||||
|
||||
runner.test('Build args for plan mode', () => {
|
||||
const args = ['-p', 'test', '--settings', '/path/settings.json'];
|
||||
const permissionMode = 'plan';
|
||||
|
||||
if (permissionMode && permissionMode !== 'default') {
|
||||
if (permissionMode === 'bypassPermissions') {
|
||||
args.push('--dangerously-skip-permissions');
|
||||
} else {
|
||||
args.push('--permission-mode', permissionMode);
|
||||
}
|
||||
}
|
||||
|
||||
assert(args.includes('--permission-mode'), 'Should have permission-mode flag');
|
||||
assert(args.includes('plan'), 'Should have plan value');
|
||||
});
|
||||
|
||||
runner.test('Build args for bypassPermissions mode', () => {
|
||||
const args = ['-p', 'test', '--settings', '/path/settings.json'];
|
||||
const permissionMode = 'bypassPermissions';
|
||||
|
||||
if (permissionMode && permissionMode !== 'default') {
|
||||
if (permissionMode === 'bypassPermissions') {
|
||||
args.push('--dangerously-skip-permissions');
|
||||
} else {
|
||||
args.push('--permission-mode', permissionMode);
|
||||
}
|
||||
}
|
||||
|
||||
assert(args.includes('--dangerously-skip-permissions'), 'Should have bypass flag');
|
||||
assert(!args.includes('--permission-mode'), 'Should not have permission-mode flag');
|
||||
});
|
||||
|
||||
runner.test('Build args for default mode (no flag)', () => {
|
||||
const args = ['-p', 'test', '--settings', '/path/settings.json'];
|
||||
const permissionMode = 'default';
|
||||
|
||||
if (permissionMode && permissionMode !== 'default') {
|
||||
if (permissionMode === 'bypassPermissions') {
|
||||
args.push('--dangerously-skip-permissions');
|
||||
} else {
|
||||
args.push('--permission-mode', permissionMode);
|
||||
}
|
||||
}
|
||||
|
||||
assert(!args.includes('--permission-mode'), 'Should not add permission-mode for default');
|
||||
assert(!args.includes('--dangerously-skip-permissions'), 'Should not add bypass for default');
|
||||
assert(args.length === 4, 'Should only have base args');
|
||||
});
|
||||
|
||||
// Run tests
|
||||
runner.run();
|
||||
@@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const { ResultFormatter } = require('../../../bin/delegation/result-formatter');
|
||||
|
||||
/**
|
||||
* Simple test runner (no external dependencies)
|
||||
*/
|
||||
class TestRunner {
|
||||
constructor() {
|
||||
this.tests = [];
|
||||
this.passed = 0;
|
||||
this.failed = 0;
|
||||
}
|
||||
|
||||
test(name, fn) {
|
||||
this.tests.push({ name, fn });
|
||||
}
|
||||
|
||||
async run() {
|
||||
console.log('\n=== ResultFormatter Tests ===\n');
|
||||
|
||||
for (const { name, fn } of this.tests) {
|
||||
try {
|
||||
await fn();
|
||||
console.log(`[OK] ${name}`);
|
||||
this.passed++;
|
||||
} catch (error) {
|
||||
console.error(`[X] ${name}`);
|
||||
console.error(` Error: ${error.message}`);
|
||||
this.failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nResults: ${this.passed} passed, ${this.failed} failed`);
|
||||
process.exit(this.failed > 0 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assertion helpers
|
||||
*/
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message || 'Assertion failed');
|
||||
}
|
||||
|
||||
function assertIncludes(haystack, needle, message) {
|
||||
if (!haystack.includes(needle)) {
|
||||
throw new Error(message || `Expected to include "${needle}"`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run tests
|
||||
*/
|
||||
const runner = new TestRunner();
|
||||
|
||||
// Test 1: Basic formatting
|
||||
runner.test('Should format successful result', () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/home/user/project',
|
||||
exitCode: 0,
|
||||
stdout: 'Task completed successfully',
|
||||
stderr: '',
|
||||
duration: 2300,
|
||||
success: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
|
||||
assertIncludes(formatted, 'Delegated to GLM-4.6', 'Should mention model');
|
||||
assertIncludes(formatted, 'ccs:glm', 'Should mention profile');
|
||||
assertIncludes(formatted, '/home/user/project', 'Should include CWD');
|
||||
assertIncludes(formatted, '2.3s', 'Should format duration');
|
||||
assertIncludes(formatted, 'Exit Code: 0', 'Should show exit code');
|
||||
assertIncludes(formatted, '[OK]', 'Should show success');
|
||||
});
|
||||
|
||||
// Test 2: Failed result
|
||||
runner.test('Should format failed result', () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/home/user/project',
|
||||
exitCode: 1,
|
||||
stdout: 'Error occurred',
|
||||
stderr: 'Command failed',
|
||||
duration: 1500,
|
||||
success: false
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
|
||||
assertIncludes(formatted, '[X]', 'Should show failure indicator');
|
||||
assertIncludes(formatted, 'Exit Code: 1', 'Should show non-zero exit code');
|
||||
assertIncludes(formatted, 'Delegation failed', 'Should indicate failure');
|
||||
assertIncludes(formatted, 'Stderr:', 'Should include stderr section');
|
||||
assertIncludes(formatted, 'Command failed', 'Should show stderr content');
|
||||
});
|
||||
|
||||
// Test 3: Extract created files
|
||||
runner.test('Should extract created files from output', () => {
|
||||
const output = 'Created: src/auth.js\nCreated: tests/auth.test.js';
|
||||
|
||||
const { created, modified } = ResultFormatter.extractFileChanges(output);
|
||||
|
||||
assert(created.length === 2, 'Should find 2 created files');
|
||||
assertIncludes(created[0], 'src/auth.js', 'Should include first file');
|
||||
assertIncludes(created[1], 'tests/auth.test.js', 'Should include second file');
|
||||
});
|
||||
|
||||
// Test 4: Extract modified files
|
||||
runner.test('Should extract modified files from output', () => {
|
||||
const output = 'Modified: src/index.js\nUpdated: package.json';
|
||||
|
||||
const { created, modified } = ResultFormatter.extractFileChanges(output);
|
||||
|
||||
assert(modified.length === 2, 'Should find 2 modified files');
|
||||
assertIncludes(modified[0], 'src/index.js', 'Should include first file');
|
||||
assertIncludes(modified[1], 'package.json', 'Should include second file');
|
||||
});
|
||||
|
||||
// Test 5: Extract mixed file changes
|
||||
runner.test('Should extract both created and modified files', () => {
|
||||
const output = 'Created: src/new.js\nModified: src/old.js\nCreated: tests/new.test.js';
|
||||
|
||||
const { created, modified } = ResultFormatter.extractFileChanges(output);
|
||||
|
||||
assert(created.length === 2, 'Should find 2 created files');
|
||||
assert(modified.length === 1, 'Should find 1 modified file');
|
||||
});
|
||||
|
||||
// Test 6: No duplicate files in lists
|
||||
runner.test('Should not duplicate files in created/modified lists', () => {
|
||||
const output = 'Created: src/file.js\nCreated: src/file.js\nModified: src/file.js';
|
||||
|
||||
const { created, modified } = ResultFormatter.extractFileChanges(output);
|
||||
|
||||
assert(created.length === 1, 'Should deduplicate created files');
|
||||
assert(modified.length === 0, 'Should not list created files as modified');
|
||||
});
|
||||
|
||||
// Test 7: Format file lists
|
||||
runner.test('Should format file lists in output', () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/home/user/project',
|
||||
exitCode: 0,
|
||||
stdout: 'Created: src/new.js\nModified: src/old.js',
|
||||
stderr: '',
|
||||
duration: 1000,
|
||||
success: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
|
||||
assertIncludes(formatted, '[i] Created Files:', 'Should have created header');
|
||||
assertIncludes(formatted, 'src/new.js', 'Should list created file');
|
||||
assertIncludes(formatted, '[i] Modified Files:', 'Should have modified header');
|
||||
assertIncludes(formatted, 'src/old.js', 'Should list modified file');
|
||||
});
|
||||
|
||||
// Test 8: ASCII box formatting
|
||||
runner.test('Should use ASCII box characters', () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/home/user/project',
|
||||
exitCode: 0,
|
||||
stdout: 'Done',
|
||||
stderr: '',
|
||||
duration: 1000,
|
||||
success: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
|
||||
assertIncludes(formatted, '╔', 'Should have top-left corner');
|
||||
assertIncludes(formatted, '╗', 'Should have top-right corner');
|
||||
assertIncludes(formatted, '╚', 'Should have bottom-left corner');
|
||||
assertIncludes(formatted, '╝', 'Should have bottom-right corner');
|
||||
assertIncludes(formatted, '║', 'Should have vertical borders');
|
||||
assertIncludes(formatted, '═', 'Should have horizontal borders');
|
||||
});
|
||||
|
||||
// Test 9: Model display names
|
||||
runner.test('Should use correct model display names', () => {
|
||||
const glmResult = {
|
||||
profile: 'glm',
|
||||
cwd: '/test',
|
||||
exitCode: 0,
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
duration: 1000,
|
||||
success: true
|
||||
};
|
||||
|
||||
const glmFormatted = ResultFormatter.format(glmResult);
|
||||
assertIncludes(glmFormatted, 'GLM-4.6', 'Should show GLM-4.6');
|
||||
|
||||
const kimiResult = { ...glmResult, profile: 'kimi' };
|
||||
const kimiFormatted = ResultFormatter.format(kimiResult);
|
||||
assertIncludes(kimiFormatted, 'Kimi', 'Should show Kimi');
|
||||
});
|
||||
|
||||
// Test 10: Duration formatting
|
||||
runner.test('Should format duration correctly', () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/test',
|
||||
exitCode: 0,
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
duration: 12345,
|
||||
success: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
|
||||
assertIncludes(formatted, '12.3s', 'Should format to 1 decimal place');
|
||||
});
|
||||
|
||||
// Test 11: Empty output handling
|
||||
runner.test('Should handle empty output', () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/test',
|
||||
exitCode: 0,
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
duration: 1000,
|
||||
success: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
|
||||
assertIncludes(formatted, 'No output', 'Should indicate no output');
|
||||
});
|
||||
|
||||
// Test 12: Minimal format
|
||||
runner.test('Should support minimal format', () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/test',
|
||||
exitCode: 0,
|
||||
stdout: 'Done',
|
||||
stderr: '',
|
||||
duration: 1500,
|
||||
success: true
|
||||
};
|
||||
|
||||
const minimal = ResultFormatter.formatMinimal(result);
|
||||
|
||||
assertIncludes(minimal, '[OK]', 'Should show success');
|
||||
assertIncludes(minimal, 'GLM-4.6', 'Should show model');
|
||||
assertIncludes(minimal, '1.5s', 'Should show duration');
|
||||
assert(minimal.split('\n').length <= 3, 'Should be concise');
|
||||
});
|
||||
|
||||
// Test 13: Case-insensitive file pattern matching
|
||||
runner.test('Should match file patterns case-insensitively', () => {
|
||||
const output = 'CREATED: src/file.js\nMODIFIED: src/other.js';
|
||||
|
||||
const { created, modified } = ResultFormatter.extractFileChanges(output);
|
||||
|
||||
assert(created.length === 1, 'Should find created file (uppercase)');
|
||||
assert(modified.length === 1, 'Should find modified file (uppercase)');
|
||||
});
|
||||
|
||||
// Test 14: File count in info box
|
||||
runner.test('Should show file counts in info box', () => {
|
||||
const result = {
|
||||
profile: 'glm',
|
||||
cwd: '/test',
|
||||
exitCode: 0,
|
||||
stdout: 'Created: a.js\nCreated: b.js\nModified: c.js',
|
||||
stderr: '',
|
||||
duration: 1000,
|
||||
success: true
|
||||
};
|
||||
|
||||
const formatted = ResultFormatter.format(result);
|
||||
|
||||
assertIncludes(formatted, 'Files Created: 2', 'Should show created count');
|
||||
assertIncludes(formatted, 'Files Modified: 1', 'Should show modified count');
|
||||
});
|
||||
|
||||
// Run all tests
|
||||
runner.run();
|
||||
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { SessionManager } = require('../../../bin/delegation/session-manager');
|
||||
|
||||
/**
|
||||
* Test runner
|
||||
*/
|
||||
class TestRunner {
|
||||
constructor() {
|
||||
this.tests = [];
|
||||
this.passed = 0;
|
||||
this.failed = 0;
|
||||
}
|
||||
|
||||
test(name, fn) {
|
||||
this.tests.push({ name, fn });
|
||||
}
|
||||
|
||||
async run() {
|
||||
console.log('\n=== Session Manager Tests ===\n');
|
||||
|
||||
for (const { name, fn } of this.tests) {
|
||||
try {
|
||||
await fn();
|
||||
console.log(`[OK] ${name}`);
|
||||
this.passed++;
|
||||
} catch (error) {
|
||||
console.error(`[X] ${name}`);
|
||||
console.error(` Error: ${error.message}`);
|
||||
this.failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nResults: ${this.passed} passed, ${this.failed} failed`);
|
||||
process.exit(this.failed > 0 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message || 'Assertion failed');
|
||||
}
|
||||
|
||||
function assertEqual(actual, expected, message) {
|
||||
if (actual !== expected) {
|
||||
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test suite
|
||||
const runner = new TestRunner();
|
||||
|
||||
// Cleanup test sessions before/after
|
||||
const testSessionsPath = path.join(os.homedir(), '.ccs', 'delegation-sessions.json');
|
||||
function cleanupTestSessions() {
|
||||
if (fs.existsSync(testSessionsPath)) {
|
||||
fs.unlinkSync(testSessionsPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 1: Store and retrieve session
|
||||
*/
|
||||
runner.test('Store new session', () => {
|
||||
cleanupTestSessions();
|
||||
|
||||
const mgr = new SessionManager();
|
||||
mgr.storeSession('glm', {
|
||||
sessionId: 'test123',
|
||||
totalCost: 0.0025,
|
||||
cwd: '/home/test'
|
||||
});
|
||||
|
||||
const session = mgr.getLastSession('glm');
|
||||
assert(session, 'Session should exist');
|
||||
assertEqual(session.sessionId, 'test123', 'Session ID should match');
|
||||
assertEqual(session.totalCost, 0.0025, 'Cost should match');
|
||||
assertEqual(session.turns, 1, 'Should have 1 turn initially');
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 2: Update session
|
||||
*/
|
||||
runner.test('Update existing session', () => {
|
||||
const mgr = new SessionManager();
|
||||
|
||||
// Store initial
|
||||
mgr.storeSession('glm', {
|
||||
sessionId: 'test456',
|
||||
totalCost: 0.001,
|
||||
cwd: '/home/test'
|
||||
});
|
||||
|
||||
// Update
|
||||
mgr.updateSession('glm', 'test456', {
|
||||
totalCost: 0.002
|
||||
});
|
||||
|
||||
const session = mgr.getLastSession('glm');
|
||||
assertEqual(session.totalCost, 0.003, 'Cost should be aggregated (0.001 + 0.002)');
|
||||
assertEqual(session.turns, 2, 'Should have 2 turns');
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 3: Multiple profiles
|
||||
*/
|
||||
runner.test('Manage multiple profiles', () => {
|
||||
const mgr = new SessionManager();
|
||||
|
||||
mgr.storeSession('glm', {
|
||||
sessionId: 'glm123',
|
||||
totalCost: 0.001,
|
||||
cwd: '/home/test'
|
||||
});
|
||||
|
||||
mgr.storeSession('kimi', {
|
||||
sessionId: 'kimi123',
|
||||
totalCost: 0.002,
|
||||
cwd: '/home/test'
|
||||
});
|
||||
|
||||
const glmSession = mgr.getLastSession('glm');
|
||||
const kimiSession = mgr.getLastSession('kimi');
|
||||
|
||||
assertEqual(glmSession.sessionId, 'glm123', 'GLM session should be separate');
|
||||
assertEqual(kimiSession.sessionId, 'kimi123', 'Kimi session should be separate');
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 4: No session for profile
|
||||
*/
|
||||
runner.test('Return null for non-existent profile', () => {
|
||||
const mgr = new SessionManager();
|
||||
|
||||
const session = mgr.getLastSession('nonexistent');
|
||||
assertEqual(session, null, 'Should return null for unknown profile');
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 5: Clear profile
|
||||
*/
|
||||
runner.test('Clear profile sessions', () => {
|
||||
const mgr = new SessionManager();
|
||||
|
||||
mgr.storeSession('glm', {
|
||||
sessionId: 'test789',
|
||||
totalCost: 0.001,
|
||||
cwd: '/home/test'
|
||||
});
|
||||
|
||||
mgr.clearProfile('glm');
|
||||
|
||||
const session = mgr.getLastSession('glm');
|
||||
assertEqual(session, null, 'Session should be cleared');
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 6: Cleanup expired sessions
|
||||
*/
|
||||
runner.test('Cleanup expired sessions', () => {
|
||||
const mgr = new SessionManager();
|
||||
|
||||
// Store session with old timestamp (31 days ago)
|
||||
const sessions = {};
|
||||
const oldTime = Date.now() - (31 * 24 * 60 * 60 * 1000);
|
||||
sessions['glm:latest'] = {
|
||||
sessionId: 'old123',
|
||||
profile: 'glm',
|
||||
startTime: oldTime,
|
||||
lastTurnTime: oldTime,
|
||||
totalCost: 0.001,
|
||||
turns: 1,
|
||||
cwd: '/home/test'
|
||||
};
|
||||
|
||||
// Save manually
|
||||
const dir = path.dirname(mgr.sessionsPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(mgr.sessionsPath, JSON.stringify(sessions));
|
||||
|
||||
// Cleanup
|
||||
mgr.cleanupExpired();
|
||||
|
||||
const session = mgr.getLastSession('glm');
|
||||
assertEqual(session, null, 'Expired session should be removed');
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 7: Don't cleanup recent sessions
|
||||
*/
|
||||
runner.test('Keep recent sessions during cleanup', () => {
|
||||
const mgr = new SessionManager();
|
||||
|
||||
mgr.storeSession('glm', {
|
||||
sessionId: 'recent123',
|
||||
totalCost: 0.001,
|
||||
cwd: '/home/test'
|
||||
});
|
||||
|
||||
mgr.cleanupExpired();
|
||||
|
||||
const session = mgr.getLastSession('glm');
|
||||
assert(session, 'Recent session should not be removed');
|
||||
assertEqual(session.sessionId, 'recent123');
|
||||
});
|
||||
|
||||
// Cleanup after all tests
|
||||
runner.run().finally(() => {
|
||||
cleanupTestSessions();
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { SettingsParser } = require('../../../bin/delegation/settings-parser');
|
||||
|
||||
/**
|
||||
* Test runner
|
||||
*/
|
||||
class TestRunner {
|
||||
constructor() {
|
||||
this.tests = [];
|
||||
this.passed = 0;
|
||||
this.failed = 0;
|
||||
}
|
||||
|
||||
test(name, fn) {
|
||||
this.tests.push({ name, fn });
|
||||
}
|
||||
|
||||
async run() {
|
||||
console.log('\n=== Settings Parser Tests ===\n');
|
||||
|
||||
for (const { name, fn } of this.tests) {
|
||||
try {
|
||||
await fn();
|
||||
console.log(`[OK] ${name}`);
|
||||
this.passed++;
|
||||
} catch (error) {
|
||||
console.error(`[X] ${name}`);
|
||||
console.error(` Error: ${error.message}`);
|
||||
this.failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nResults: ${this.passed} passed, ${this.failed} failed`);
|
||||
process.exit(this.failed > 0 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message || 'Assertion failed');
|
||||
}
|
||||
|
||||
function assertEqual(actual, expected, message) {
|
||||
if (actual !== expected) {
|
||||
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test suite
|
||||
const runner = new TestRunner();
|
||||
|
||||
// Test fixture directory
|
||||
const testDir = path.join(os.tmpdir(), 'ccs-test-settings');
|
||||
const claudeDir = path.join(testDir, '.claude');
|
||||
|
||||
// Cleanup helpers
|
||||
function setupTestDir() {
|
||||
if (fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true });
|
||||
}
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
}
|
||||
|
||||
function cleanupTestDir() {
|
||||
if (fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 1: No settings files
|
||||
*/
|
||||
runner.test('Return empty arrays when no settings files', () => {
|
||||
setupTestDir();
|
||||
|
||||
const restrictions = SettingsParser.parseToolRestrictions(testDir);
|
||||
|
||||
assertEqual(restrictions.allowedTools.length, 0, 'Should have 0 allowed tools');
|
||||
assertEqual(restrictions.disallowedTools.length, 0, 'Should have 0 disallowed tools');
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 2: Parse shared settings.json
|
||||
*/
|
||||
runner.test('Parse shared settings.json', () => {
|
||||
setupTestDir();
|
||||
|
||||
const settingsPath = path.join(claudeDir, 'settings.json');
|
||||
fs.writeFileSync(settingsPath, JSON.stringify({
|
||||
permissions: {
|
||||
allow: ['Bash(git:*)', 'Read'],
|
||||
deny: ['Bash(rm:*)']
|
||||
}
|
||||
}));
|
||||
|
||||
const restrictions = SettingsParser.parseToolRestrictions(testDir);
|
||||
|
||||
assertEqual(restrictions.allowedTools.length, 2, 'Should have 2 allowed tools');
|
||||
assertEqual(restrictions.disallowedTools.length, 1, 'Should have 1 disallowed tool');
|
||||
assert(restrictions.allowedTools.includes('Bash(git:*)'), 'Should include git bash');
|
||||
assert(restrictions.disallowedTools.includes('Bash(rm:*)'), 'Should include rm deny');
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 3: Parse local settings overriding shared
|
||||
*/
|
||||
runner.test('Local settings override shared', () => {
|
||||
setupTestDir();
|
||||
|
||||
// Shared settings
|
||||
fs.writeFileSync(path.join(claudeDir, 'settings.json'), JSON.stringify({
|
||||
permissions: {
|
||||
allow: ['Read'],
|
||||
deny: []
|
||||
}
|
||||
}));
|
||||
|
||||
// Local settings (adds more permissions)
|
||||
fs.writeFileSync(path.join(claudeDir, 'settings.local.json'), JSON.stringify({
|
||||
permissions: {
|
||||
allow: ['Bash(git:*)'],
|
||||
deny: ['Bash(rm:*)']
|
||||
}
|
||||
}));
|
||||
|
||||
const restrictions = SettingsParser.parseToolRestrictions(testDir);
|
||||
|
||||
assertEqual(restrictions.allowedTools.length, 2, 'Should merge allowed tools');
|
||||
assert(restrictions.allowedTools.includes('Read'), 'Should have shared Read');
|
||||
assert(restrictions.allowedTools.includes('Bash(git:*)'), 'Should have local git');
|
||||
assertEqual(restrictions.disallowedTools.length, 1, 'Should have local deny');
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 4: Handle malformed JSON
|
||||
*/
|
||||
runner.test('Handle malformed JSON gracefully', () => {
|
||||
setupTestDir();
|
||||
|
||||
const settingsPath = path.join(claudeDir, 'settings.json');
|
||||
fs.writeFileSync(settingsPath, '{ invalid json }');
|
||||
|
||||
// Should not throw
|
||||
const restrictions = SettingsParser.parseToolRestrictions(testDir);
|
||||
|
||||
assertEqual(restrictions.allowedTools.length, 0, 'Should return empty arrays on parse error');
|
||||
assertEqual(restrictions.disallowedTools.length, 0);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 5: Handle missing permissions key
|
||||
*/
|
||||
runner.test('Handle settings without permissions key', () => {
|
||||
setupTestDir();
|
||||
|
||||
const settingsPath = path.join(claudeDir, 'settings.json');
|
||||
fs.writeFileSync(settingsPath, JSON.stringify({
|
||||
someOtherKey: 'value'
|
||||
}));
|
||||
|
||||
const restrictions = SettingsParser.parseToolRestrictions(testDir);
|
||||
|
||||
assertEqual(restrictions.allowedTools.length, 0, 'Should handle missing permissions');
|
||||
assertEqual(restrictions.disallowedTools.length, 0);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 6: Handle empty permissions arrays
|
||||
*/
|
||||
runner.test('Handle empty permissions arrays', () => {
|
||||
setupTestDir();
|
||||
|
||||
const settingsPath = path.join(claudeDir, 'settings.json');
|
||||
fs.writeFileSync(settingsPath, JSON.stringify({
|
||||
permissions: {
|
||||
allow: [],
|
||||
deny: []
|
||||
}
|
||||
}));
|
||||
|
||||
const restrictions = SettingsParser.parseToolRestrictions(testDir);
|
||||
|
||||
assertEqual(restrictions.allowedTools.length, 0);
|
||||
assertEqual(restrictions.disallowedTools.length, 0);
|
||||
});
|
||||
|
||||
// Run tests and cleanup
|
||||
runner.run().finally(() => {
|
||||
cleanupTestDir();
|
||||
});
|
||||
Reference in New Issue
Block a user