@kaitranntt/ccs 3.5.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ /**
8
+ * Parses Claude Code settings for tool restrictions
9
+ */
10
+ class SettingsParser {
11
+ /**
12
+ * Parse default permission mode from project settings
13
+ * @param {string} projectDir - Project directory (usually cwd)
14
+ * @returns {string} Default permission mode (e.g., 'acceptEdits', 'bypassPermissions', 'plan', 'default')
15
+ */
16
+ static parseDefaultPermissionMode(projectDir) {
17
+ const settings = this._loadSettings(projectDir);
18
+ const permissions = settings.permissions || {};
19
+
20
+ // Priority: local > shared > fallback to 'acceptEdits'
21
+ const defaultMode = permissions.defaultMode || 'acceptEdits';
22
+
23
+ if (process.env.CCS_DEBUG) {
24
+ console.error(`[i] Permission mode from settings: ${defaultMode}`);
25
+ }
26
+
27
+ return defaultMode;
28
+ }
29
+
30
+ /**
31
+ * Parse project settings for tool restrictions
32
+ * @param {string} projectDir - Project directory (usually cwd)
33
+ * @returns {Object} { allowedTools: string[], disallowedTools: string[] }
34
+ */
35
+ static parseToolRestrictions(projectDir) {
36
+ const settings = this._loadSettings(projectDir);
37
+ const permissions = settings.permissions || {};
38
+
39
+ const allowed = permissions.allow || [];
40
+ const denied = permissions.deny || [];
41
+
42
+ if (process.env.CCS_DEBUG) {
43
+ console.error(`[i] Tool restrictions: ${allowed.length} allowed, ${denied.length} denied`);
44
+ }
45
+
46
+ return {
47
+ allowedTools: allowed,
48
+ disallowedTools: denied
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Load and merge settings files (local overrides shared)
54
+ * @param {string} projectDir - Project directory
55
+ * @returns {Object} Merged settings
56
+ * @private
57
+ */
58
+ static _loadSettings(projectDir) {
59
+ const claudeDir = path.join(projectDir, '.claude');
60
+ const sharedPath = path.join(claudeDir, 'settings.json');
61
+ const localPath = path.join(claudeDir, 'settings.local.json');
62
+
63
+ // Load shared settings
64
+ const shared = this._readJsonSafe(sharedPath) || {};
65
+
66
+ // Load local settings (overrides shared)
67
+ const local = this._readJsonSafe(localPath) || {};
68
+
69
+ // Merge permissions (local overrides shared)
70
+ return {
71
+ permissions: {
72
+ allow: [
73
+ ...(shared.permissions?.allow || []),
74
+ ...(local.permissions?.allow || [])
75
+ ],
76
+ deny: [
77
+ ...(shared.permissions?.deny || []),
78
+ ...(local.permissions?.deny || [])
79
+ ],
80
+ // Local defaultMode takes priority over shared
81
+ defaultMode: local.permissions?.defaultMode || shared.permissions?.defaultMode || null
82
+ }
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Read JSON file safely (no throw)
88
+ * @param {string} filePath - Path to JSON file
89
+ * @returns {Object|null} Parsed JSON or null
90
+ * @private
91
+ */
92
+ static _readJsonSafe(filePath) {
93
+ try {
94
+ if (!fs.existsSync(filePath)) {
95
+ return null;
96
+ }
97
+
98
+ const content = fs.readFileSync(filePath, 'utf8');
99
+ return JSON.parse(content);
100
+ } catch (error) {
101
+ if (process.env.CCS_DEBUG) {
102
+ console.warn(`[!] Failed to read settings: ${filePath}: ${error.message}`);
103
+ }
104
+ return null;
105
+ }
106
+ }
107
+ }
108
+
109
+ module.exports = { SettingsParser };
@@ -61,7 +61,9 @@ class Doctor {
61
61
  this.checkClaudeSettings();
62
62
  this.checkProfiles();
63
63
  this.checkInstances();
64
+ this.checkDelegation();
64
65
  this.checkPermissions();
66
+ this.checkCcsSymlinks();
65
67
 
66
68
  this.showReport();
67
69
  return this.results;
@@ -269,7 +271,64 @@ class Doctor {
269
271
  }
270
272
 
271
273
  /**
272
- * Check 7: File permissions
274
+ * Check 7: Delegation system
275
+ */
276
+ checkDelegation() {
277
+ process.stdout.write('[?] Checking delegation... ');
278
+
279
+ // Check if delegation-rules.json exists
280
+ const delegationRulesPath = path.join(this.ccsDir, 'delegation-rules.json');
281
+ const hasDelegationRules = fs.existsSync(delegationRulesPath);
282
+
283
+ // Check if delegation commands exist
284
+ const sharedCommandsDir = path.join(this.ccsDir, 'shared', 'commands', 'ccs');
285
+ const hasGlmCommand = fs.existsSync(path.join(sharedCommandsDir, 'glm.md'));
286
+ const hasKimiCommand = fs.existsSync(path.join(sharedCommandsDir, 'kimi.md'));
287
+ const hasCreateCommand = fs.existsSync(path.join(sharedCommandsDir, 'create.md'));
288
+
289
+ if (!hasGlmCommand || !hasKimiCommand || !hasCreateCommand) {
290
+ console.log(colored('[!]', 'yellow'), '(not installed)');
291
+ this.results.addCheck(
292
+ 'Delegation',
293
+ 'warning',
294
+ 'Delegation commands not found',
295
+ 'Install with: npm install -g @kaitranntt/ccs --force'
296
+ );
297
+ return;
298
+ }
299
+
300
+ // Check profile validity using DelegationValidator
301
+ const { DelegationValidator } = require('../utils/delegation-validator');
302
+ const readyProfiles = [];
303
+
304
+ for (const profile of ['glm', 'kimi']) {
305
+ const validation = DelegationValidator.validate(profile);
306
+ if (validation.valid) {
307
+ readyProfiles.push(profile);
308
+ }
309
+ }
310
+
311
+ if (readyProfiles.length === 0) {
312
+ console.log(colored('[!]', 'yellow'), '(no profiles ready)');
313
+ this.results.addCheck(
314
+ 'Delegation',
315
+ 'warning',
316
+ 'Delegation installed but no profiles configured',
317
+ 'Configure profiles with valid API keys (not placeholders)'
318
+ );
319
+ return;
320
+ }
321
+
322
+ console.log(colored('[OK]', 'green'), `(${readyProfiles.join(', ')} ready)`);
323
+ this.results.addCheck(
324
+ 'Delegation',
325
+ 'success',
326
+ `${readyProfiles.length} profile(s) ready: ${readyProfiles.join(', ')}`
327
+ );
328
+ }
329
+
330
+ /**
331
+ * Check 8: File permissions
273
332
  */
274
333
  checkPermissions() {
275
334
  process.stdout.write('[?] Checking permissions... ');
@@ -292,6 +351,40 @@ class Doctor {
292
351
  }
293
352
  }
294
353
 
354
+ /**
355
+ * Check 9: CCS symlinks to ~/.claude/
356
+ */
357
+ checkCcsSymlinks() {
358
+ process.stdout.write('[?] Checking CCS symlinks... ');
359
+
360
+ try {
361
+ const ClaudeSymlinkManager = require('../utils/claude-symlink-manager');
362
+ const manager = new ClaudeSymlinkManager();
363
+ const health = manager.checkHealth();
364
+
365
+ if (health.healthy) {
366
+ console.log(colored('[OK]', 'green'));
367
+ this.results.addCheck('CCS Symlinks', 'success', 'All CCS items properly symlinked');
368
+ } else {
369
+ console.log(colored('[!]', 'yellow'));
370
+ this.results.addCheck(
371
+ 'CCS Symlinks',
372
+ 'warning',
373
+ health.issues.join(', '),
374
+ 'Run: ccs update'
375
+ );
376
+ }
377
+ } catch (e) {
378
+ console.log(colored('[!]', 'yellow'));
379
+ this.results.addCheck(
380
+ 'CCS Symlinks',
381
+ 'warning',
382
+ 'Could not check CCS symlinks: ' + e.message,
383
+ 'Run: ccs update'
384
+ );
385
+ }
386
+ }
387
+
295
388
  /**
296
389
  * Show health check report
297
390
  */
@@ -0,0 +1,238 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ /**
8
+ * ClaudeSymlinkManager - Manages selective symlinks from ~/.ccs/.claude/ to ~/.claude/
9
+ * v4.1.0: Selective symlinking for CCS items
10
+ *
11
+ * Purpose: Ship CCS items (.claude/) with package and symlink them to user's ~/.claude/
12
+ * Architecture:
13
+ * - ~/.ccs/.claude/* (source, ships with CCS)
14
+ * - ~/.claude/* (target, gets selective symlinks)
15
+ * - ~/.ccs/shared/ (UNTOUCHED, existing profile mechanism)
16
+ *
17
+ * Symlink Chain:
18
+ * profile -> ~/.ccs/shared/ -> ~/.claude/ (which has symlinks to ~/.ccs/.claude/)
19
+ */
20
+ class ClaudeSymlinkManager {
21
+ constructor() {
22
+ this.homeDir = os.homedir();
23
+ this.ccsClaudeDir = path.join(this.homeDir, '.ccs', '.claude');
24
+ this.userClaudeDir = path.join(this.homeDir, '.claude');
25
+
26
+ // CCS items to symlink (selective, item-level)
27
+ this.ccsItems = [
28
+ { source: 'commands/ccs', target: 'commands/ccs', type: 'directory' },
29
+ { source: 'skills/ccs-delegation', target: 'skills/ccs-delegation', type: 'directory' },
30
+ { source: 'agents/ccs-delegator.md', target: 'agents/ccs-delegator.md', type: 'file' }
31
+ ];
32
+ }
33
+
34
+ /**
35
+ * Install CCS items to user's ~/.claude/ via selective symlinks
36
+ * Safe: backs up existing files before creating symlinks
37
+ */
38
+ install() {
39
+ // Ensure ~/.ccs/.claude/ exists (should be shipped with package)
40
+ if (!fs.existsSync(this.ccsClaudeDir)) {
41
+ console.log('[!] CCS .claude/ directory not found, skipping symlink installation');
42
+ return;
43
+ }
44
+
45
+ // Create ~/.claude/ if missing
46
+ if (!fs.existsSync(this.userClaudeDir)) {
47
+ console.log('[i] Creating ~/.claude/ directory');
48
+ fs.mkdirSync(this.userClaudeDir, { recursive: true, mode: 0o700 });
49
+ }
50
+
51
+ // Install each CCS item
52
+ for (const item of this.ccsItems) {
53
+ this._installItem(item);
54
+ }
55
+
56
+ console.log('[OK] CCS items installed to ~/.claude/');
57
+ }
58
+
59
+ /**
60
+ * Install a single CCS item with conflict handling
61
+ * @param {Object} item - Item descriptor {source, target, type}
62
+ * @private
63
+ */
64
+ _installItem(item) {
65
+ const sourcePath = path.join(this.ccsClaudeDir, item.source);
66
+ const targetPath = path.join(this.userClaudeDir, item.target);
67
+ const targetDir = path.dirname(targetPath);
68
+
69
+ // Ensure source exists
70
+ if (!fs.existsSync(sourcePath)) {
71
+ console.log(`[!] Source not found: ${item.source}, skipping`);
72
+ return;
73
+ }
74
+
75
+ // Create target parent directory if needed
76
+ if (!fs.existsSync(targetDir)) {
77
+ fs.mkdirSync(targetDir, { recursive: true, mode: 0o700 });
78
+ }
79
+
80
+ // Check if target already exists
81
+ if (fs.existsSync(targetPath)) {
82
+ // Check if it's already the correct symlink
83
+ if (this._isOurSymlink(targetPath, sourcePath)) {
84
+ return; // Already correct, skip
85
+ }
86
+
87
+ // Backup existing file/directory
88
+ this._backupItem(targetPath);
89
+ }
90
+
91
+ // Create symlink
92
+ try {
93
+ const symlinkType = item.type === 'directory' ? 'dir' : 'file';
94
+ fs.symlinkSync(sourcePath, targetPath, symlinkType);
95
+ console.log(`[OK] Symlinked ${item.target}`);
96
+ } catch (err) {
97
+ // Windows fallback: stub for now, full implementation in v4.2
98
+ if (process.platform === 'win32') {
99
+ console.log(`[!] Symlink failed for ${item.target} (Windows fallback deferred to v4.2)`);
100
+ console.log(`[i] Enable Developer Mode or wait for next update`);
101
+ } else {
102
+ console.log(`[!] Failed to symlink ${item.target}: ${err.message}`);
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Check if target is already the correct symlink pointing to source
109
+ * @param {string} targetPath - Target path to check
110
+ * @param {string} expectedSource - Expected source path
111
+ * @returns {boolean} True if target is correct symlink
112
+ * @private
113
+ */
114
+ _isOurSymlink(targetPath, expectedSource) {
115
+ try {
116
+ const stats = fs.lstatSync(targetPath);
117
+
118
+ if (!stats.isSymbolicLink()) {
119
+ return false;
120
+ }
121
+
122
+ const actualTarget = fs.readlinkSync(targetPath);
123
+ const resolvedTarget = path.resolve(path.dirname(targetPath), actualTarget);
124
+
125
+ return resolvedTarget === expectedSource;
126
+ } catch (err) {
127
+ return false;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Backup existing item before replacing with symlink
133
+ * @param {string} itemPath - Path to item to backup
134
+ * @private
135
+ */
136
+ _backupItem(itemPath) {
137
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
138
+ const backupPath = `${itemPath}.backup-${timestamp}`;
139
+
140
+ try {
141
+ // If backup already exists, use counter
142
+ let finalBackupPath = backupPath;
143
+ let counter = 1;
144
+ while (fs.existsSync(finalBackupPath)) {
145
+ finalBackupPath = `${backupPath}-${counter}`;
146
+ counter++;
147
+ }
148
+
149
+ fs.renameSync(itemPath, finalBackupPath);
150
+ console.log(`[i] Backed up existing item to ${path.basename(finalBackupPath)}`);
151
+ } catch (err) {
152
+ console.log(`[!] Failed to backup ${itemPath}: ${err.message}`);
153
+ throw err; // Don't proceed if backup fails
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Uninstall CCS items from ~/.claude/ (remove symlinks only)
159
+ * Safe: only removes items that are CCS symlinks
160
+ */
161
+ uninstall() {
162
+ let removed = 0;
163
+
164
+ for (const item of this.ccsItems) {
165
+ const targetPath = path.join(this.userClaudeDir, item.target);
166
+ const sourcePath = path.join(this.ccsClaudeDir, item.source);
167
+
168
+ // Only remove if it's our symlink
169
+ if (fs.existsSync(targetPath) && this._isOurSymlink(targetPath, sourcePath)) {
170
+ try {
171
+ fs.unlinkSync(targetPath);
172
+ console.log(`[OK] Removed ${item.target}`);
173
+ removed++;
174
+ } catch (err) {
175
+ console.log(`[!] Failed to remove ${item.target}: ${err.message}`);
176
+ }
177
+ }
178
+ }
179
+
180
+ if (removed > 0) {
181
+ console.log(`[OK] Removed ${removed} CCS items from ~/.claude/`);
182
+ } else {
183
+ console.log('[i] No CCS items to remove');
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Check symlink health and report issues
189
+ * Used by 'ccs doctor' command
190
+ * @returns {Object} Health check results {healthy: boolean, issues: string[]}
191
+ */
192
+ checkHealth() {
193
+ const issues = [];
194
+ let healthy = true;
195
+
196
+ // Check if ~/.ccs/.claude/ exists
197
+ if (!fs.existsSync(this.ccsClaudeDir)) {
198
+ issues.push('CCS .claude/ directory missing (reinstall CCS)');
199
+ healthy = false;
200
+ return { healthy, issues };
201
+ }
202
+
203
+ // Check each item
204
+ for (const item of this.ccsItems) {
205
+ const sourcePath = path.join(this.ccsClaudeDir, item.source);
206
+ const targetPath = path.join(this.userClaudeDir, item.target);
207
+
208
+ // Check source exists
209
+ if (!fs.existsSync(sourcePath)) {
210
+ issues.push(`Source missing: ${item.source}`);
211
+ healthy = false;
212
+ continue;
213
+ }
214
+
215
+ // Check target
216
+ if (!fs.existsSync(targetPath)) {
217
+ issues.push(`Not installed: ${item.target} (run 'ccs update' to install)`);
218
+ healthy = false;
219
+ } else if (!this._isOurSymlink(targetPath, sourcePath)) {
220
+ issues.push(`Not a CCS symlink: ${item.target} (run 'ccs update' to fix)`);
221
+ healthy = false;
222
+ }
223
+ }
224
+
225
+ return { healthy, issues };
226
+ }
227
+
228
+ /**
229
+ * Re-install symlinks (used by 'ccs update' command)
230
+ * Same as install() but with explicit re-installation message
231
+ */
232
+ update() {
233
+ console.log('[i] Updating CCS items in ~/.claude/...');
234
+ this.install();
235
+ }
236
+ }
237
+
238
+ module.exports = ClaudeSymlinkManager;
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ /**
9
+ * Validates delegation profiles for CCS delegation system
10
+ * Ensures profiles exist and have valid API keys configured
11
+ */
12
+ class DelegationValidator {
13
+ /**
14
+ * Validate a delegation profile
15
+ * @param {string} profileName - Name of profile to validate (e.g., 'glm', 'kimi')
16
+ * @returns {Object} Validation result { valid: boolean, error?: string, settingsPath?: string }
17
+ */
18
+ static validate(profileName) {
19
+ const homeDir = os.homedir();
20
+ const settingsPath = path.join(homeDir, '.ccs', `${profileName}.settings.json`);
21
+
22
+ // Check if profile directory exists
23
+ if (!fs.existsSync(settingsPath)) {
24
+ return {
25
+ valid: false,
26
+ error: `Profile not found: ${profileName}`,
27
+ suggestion: `Profile settings missing at: ${settingsPath}\n\n` +
28
+ `To set up ${profileName} profile:\n` +
29
+ ` 1. Copy base settings: cp config/base-${profileName}.settings.json ~/.ccs/${profileName}.settings.json\n` +
30
+ ` 2. Edit settings: Edit ~/.ccs/${profileName}.settings.json\n` +
31
+ ` 3. Set your API key in ANTHROPIC_AUTH_TOKEN field`
32
+ };
33
+ }
34
+
35
+ // Read and parse settings.json
36
+ let settings;
37
+ try {
38
+ const settingsContent = fs.readFileSync(settingsPath, 'utf8');
39
+ settings = JSON.parse(settingsContent);
40
+ } catch (error) {
41
+ return {
42
+ valid: false,
43
+ error: `Failed to parse settings.json for ${profileName}`,
44
+ suggestion: `Settings file is corrupted or invalid JSON.\n\n` +
45
+ `Location: ${settingsPath}\n` +
46
+ `Parse error: ${error.message}\n\n` +
47
+ `Fix: Restore from base config:\n` +
48
+ ` cp config/base-${profileName}.settings.json ~/.ccs/${profileName}.settings.json`
49
+ };
50
+ }
51
+
52
+ // Validate API key exists and is not default
53
+ const apiKey = settings.env?.ANTHROPIC_AUTH_TOKEN;
54
+
55
+ if (!apiKey) {
56
+ return {
57
+ valid: false,
58
+ error: `API key not configured for ${profileName}`,
59
+ suggestion: `Missing ANTHROPIC_AUTH_TOKEN in settings.\n\n` +
60
+ `Edit: ${settingsPath}\n` +
61
+ `Set: env.ANTHROPIC_AUTH_TOKEN to your API key`
62
+ };
63
+ }
64
+
65
+ // Check for default placeholder values
66
+ const defaultPlaceholders = [
67
+ 'YOUR_GLM_API_KEY_HERE',
68
+ 'YOUR_KIMI_API_KEY_HERE',
69
+ 'YOUR_API_KEY_HERE',
70
+ 'your-api-key-here',
71
+ 'PLACEHOLDER'
72
+ ];
73
+
74
+ if (defaultPlaceholders.some(placeholder => apiKey.includes(placeholder))) {
75
+ return {
76
+ valid: false,
77
+ error: `Default API key placeholder detected for ${profileName}`,
78
+ suggestion: `API key is still set to default placeholder.\n\n` +
79
+ `To configure your profile:\n` +
80
+ ` 1. Edit: ${settingsPath}\n` +
81
+ ` 2. Replace ANTHROPIC_AUTH_TOKEN with your actual API key\n\n` +
82
+ `Get API key:\n` +
83
+ ` GLM: https://z.ai/manage-apikey/apikey-list\n` +
84
+ ` Kimi: https://platform.moonshot.cn/console/api-keys`
85
+ };
86
+ }
87
+
88
+ // Validation passed
89
+ return {
90
+ valid: true,
91
+ settingsPath,
92
+ apiKey: apiKey.substring(0, 8) + '...' // Show first 8 chars for verification
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Format validation error for display
98
+ * @param {Object} result - Validation result from validate()
99
+ * @returns {string} Formatted error message
100
+ */
101
+ static formatError(result) {
102
+ if (result.valid) {
103
+ return '';
104
+ }
105
+
106
+ let message = `\n[X] ${result.error}\n\n`;
107
+
108
+ if (result.suggestion) {
109
+ message += `${result.suggestion}\n`;
110
+ }
111
+
112
+ return message;
113
+ }
114
+
115
+ /**
116
+ * Check if profile is delegation-ready (shorthand)
117
+ * @param {string} profileName - Profile to check
118
+ * @returns {boolean} True if ready for delegation
119
+ */
120
+ static isReady(profileName) {
121
+ const result = this.validate(profileName);
122
+ return result.valid;
123
+ }
124
+
125
+ /**
126
+ * Get all delegation-ready profiles
127
+ * @returns {Array<string>} List of profile names ready for delegation
128
+ */
129
+ static getReadyProfiles() {
130
+ const homeDir = os.homedir();
131
+ const ccsDir = path.join(homeDir, '.ccs');
132
+
133
+ if (!fs.existsSync(ccsDir)) {
134
+ return [];
135
+ }
136
+
137
+ const profiles = [];
138
+ const entries = fs.readdirSync(ccsDir, { withFileTypes: true });
139
+
140
+ // Look for *.settings.json files
141
+ for (const entry of entries) {
142
+ if (entry.isFile() && entry.name.endsWith('.settings.json')) {
143
+ const profileName = entry.name.replace('.settings.json', '');
144
+ if (this.isReady(profileName)) {
145
+ profiles.push(profileName);
146
+ }
147
+ }
148
+ }
149
+
150
+ return profiles;
151
+ }
152
+ }
153
+
154
+ module.exports = { DelegationValidator };
package/lib/ccs CHANGED
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  # Version (updated by scripts/bump-version.sh)
5
- CCS_VERSION="3.5.0"
5
+ CCS_VERSION="4.1.0"
6
6
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
7
  readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
8
8
  readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
@@ -187,6 +187,13 @@ show_help() {
187
187
  echo -e " ${YELLOW}ccs work${RESET} Switch to work account"
188
188
  echo -e " ${YELLOW}ccs personal${RESET} Switch to personal account"
189
189
  echo ""
190
+ echo -e "${CYAN}Delegation (Token Optimization):${RESET}"
191
+ echo -e " ${YELLOW}/ccs:glm \"task\"${RESET} Delegate to GLM-4.6 within Claude session"
192
+ echo -e " ${YELLOW}/ccs:kimi \"task\"${RESET} Delegate to Kimi for long context"
193
+ echo -e " ${YELLOW}/ccs:create m2${RESET} Create custom delegation command"
194
+ echo -e " Use delegation to save tokens on simple tasks"
195
+ echo -e " Commands work inside Claude Code sessions only"
196
+ echo ""
190
197
  echo -e "${CYAN}Diagnostics:${RESET}"
191
198
  echo -e " ${YELLOW}ccs doctor${RESET} Run health check and diagnostics"
192
199
  echo ""
@@ -522,6 +529,33 @@ show_version() {
522
529
  # Simple config display
523
530
  local config="${CCS_CONFIG:-$HOME/.ccs/config.json}"
524
531
  echo -e " ${CYAN}Config:${RESET} ${config}"
532
+
533
+ # Delegation status
534
+ local delegation_rules="$HOME/.ccs/delegation-rules.json"
535
+ if [[ -f "$delegation_rules" ]]; then
536
+ echo -e " ${CYAN}Delegation:${RESET} Enabled"
537
+
538
+ # Check which profiles are delegation-ready
539
+ local ready_profiles=()
540
+ for profile in glm kimi; do
541
+ local settings_file="$HOME/.ccs/profiles/$profile/settings.json"
542
+ if [[ -f "$settings_file" ]]; then
543
+ # Check if API key is configured (not a placeholder)
544
+ local api_key=$(jq -r '.env.ANTHROPIC_AUTH_TOKEN // empty' "$settings_file" 2>/dev/null)
545
+ if [[ -n "$api_key" ]] && [[ ! "$api_key" =~ YOUR_.*_API_KEY_HERE ]]; then
546
+ ready_profiles+=("$profile")
547
+ fi
548
+ fi
549
+ done
550
+
551
+ if [[ ${#ready_profiles[@]} -gt 0 ]]; then
552
+ echo -e " ${CYAN}Ready:${RESET} ${ready_profiles[*]}"
553
+ else
554
+ echo -e " ${CYAN}Ready:${RESET} None (configure profiles first)"
555
+ fi
556
+ else
557
+ echo -e " ${CYAN}Delegation:${RESET} Not configured"
558
+ fi
525
559
  echo ""
526
560
 
527
561
  echo -e "${CYAN}Documentation:${RESET} https://github.com/kaitranntt/ccs"
package/lib/ccs.ps1 CHANGED
@@ -12,7 +12,7 @@ param(
12
12
  $ErrorActionPreference = "Stop"
13
13
 
14
14
  # Version (updated by scripts/bump-version.sh)
15
- $CcsVersion = "3.5.0"
15
+ $CcsVersion = "4.1.0"
16
16
  $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
17
17
  $ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" }
18
18
  $ProfilesJson = "$env:USERPROFILE\.ccs\profiles.json"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaitranntt/ccs",
3
- "version": "3.5.0",
3
+ "version": "4.1.0",
4
4
  "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
5
5
  "keywords": [
6
6
  "cli",
@@ -31,6 +31,7 @@
31
31
  "lib/",
32
32
  "scripts/",
33
33
  "config/",
34
+ ".claude/",
34
35
  "VERSION",
35
36
  "README.md",
36
37
  "LICENSE"