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:
Kai (Tam Nhu) Tran
2025-11-16 06:31:55 -05:00
committed by GitHub
parent f40e9647ec
commit 4df5a7d357
36 changed files with 4908 additions and 654 deletions
+117
View File
@@ -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
-180
View File
@@ -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`
+22
View File
@@ -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"
```
+22
View File
@@ -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"
```
+22
View File
@@ -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"
```
+22
View File
@@ -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"
```
+30 -158
View File
@@ -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
```
+43
View File
@@ -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
+223 -23
View File
@@ -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>
+1 -1
View File
@@ -1 +1 @@
3.5.0
4.1.0
+61
View File
@@ -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();
+189
View File
@@ -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/`
+212
View File
@@ -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;
+617
View File
@@ -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 };
+483
View File
@@ -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 };
+156
View File
@@ -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 };
+109
View File
@@ -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 };
+94 -1
View File
@@ -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
*/
+238
View File
@@ -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;
+154
View File
@@ -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 };
+492
View File
@@ -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
View File
@@ -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
View File
@@ -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
+35 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+11
View File
@@ -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();
});