@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.
- package/.claude/agents/ccs-delegator.md +117 -0
- package/.claude/commands/ccs/glm/continue.md +22 -0
- package/.claude/commands/ccs/glm.md +22 -0
- package/.claude/commands/ccs/kimi/continue.md +22 -0
- package/.claude/commands/ccs/kimi.md +22 -0
- package/.claude/skills/ccs-delegation/SKILL.md +54 -0
- package/.claude/skills/ccs-delegation/references/README.md +24 -0
- package/.claude/skills/ccs-delegation/references/delegation-guidelines.md +99 -0
- package/.claude/skills/ccs-delegation/references/headless-workflow.md +174 -0
- package/.claude/skills/ccs-delegation/references/troubleshooting.md +268 -0
- package/README.md +223 -23
- package/VERSION +1 -1
- package/bin/ccs.js +61 -0
- package/bin/delegation/README.md +189 -0
- package/bin/delegation/delegation-handler.js +212 -0
- package/bin/delegation/headless-executor.js +617 -0
- package/bin/delegation/result-formatter.js +483 -0
- package/bin/delegation/session-manager.js +156 -0
- package/bin/delegation/settings-parser.js +109 -0
- package/bin/management/doctor.js +94 -1
- package/bin/utils/claude-symlink-manager.js +238 -0
- package/bin/utils/delegation-validator.js +154 -0
- package/lib/ccs +35 -1
- package/lib/ccs.ps1 +1 -1
- package/package.json +2 -1
- package/scripts/postinstall.js +11 -0
|
@@ -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 };
|
package/bin/management/doctor.js
CHANGED
|
@@ -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:
|
|
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="
|
|
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 = "
|
|
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
|
+
"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"
|