@kaitranntt/ccs 3.4.6 → 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.ja.md +470 -146
- package/README.md +532 -145
- package/README.vi.md +484 -157
- package/VERSION +1 -1
- package/bin/auth/auth-commands.js +98 -13
- package/bin/auth/profile-detector.js +11 -6
- package/bin/ccs.js +148 -2
- 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/bin/utils/error-codes.js +59 -0
- package/bin/utils/error-manager.js +38 -32
- package/bin/utils/helpers.js +65 -1
- package/bin/utils/progress-indicator.js +111 -0
- package/bin/utils/prompt.js +134 -0
- package/bin/utils/shell-completion.js +234 -0
- package/lib/ccs +575 -25
- package/lib/ccs.ps1 +381 -20
- package/lib/error-codes.ps1 +55 -0
- package/lib/error-codes.sh +63 -0
- package/lib/progress-indicator.ps1 +120 -0
- package/lib/progress-indicator.sh +117 -0
- package/lib/prompt.ps1 +109 -0
- package/lib/prompt.sh +99 -0
- package/package.json +2 -1
- package/scripts/completion/README.md +308 -0
- package/scripts/completion/ccs.bash +81 -0
- package/scripts/completion/ccs.fish +92 -0
- package/scripts/completion/ccs.ps1 +157 -0
- package/scripts/completion/ccs.zsh +130 -0
- package/scripts/postinstall.js +35 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const { SessionManager } = require('./session-manager');
|
|
9
|
+
const { SettingsParser } = require('./settings-parser');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Headless executor for Claude CLI delegation
|
|
13
|
+
* Spawns claude with -p flag for single-turn execution
|
|
14
|
+
*/
|
|
15
|
+
class HeadlessExecutor {
|
|
16
|
+
/**
|
|
17
|
+
* Execute task via headless Claude CLI
|
|
18
|
+
* @param {string} profile - Profile name (glm, kimi, custom)
|
|
19
|
+
* @param {string} enhancedPrompt - Enhanced prompt with context
|
|
20
|
+
* @param {Object} options - Execution options
|
|
21
|
+
* @param {string} options.cwd - Working directory (absolute path)
|
|
22
|
+
* @param {number} options.timeout - Timeout in milliseconds (default: 600000 = 10 minutes)
|
|
23
|
+
* @param {string} options.outputFormat - Output format: 'stream-json' or 'text' (default: 'stream-json')
|
|
24
|
+
* @param {string} options.permissionMode - Permission mode: 'default', 'plan', 'acceptEdits', 'bypassPermissions' (default: 'acceptEdits')
|
|
25
|
+
* @param {boolean} options.resumeSession - Resume last session for profile (default: false)
|
|
26
|
+
* @param {string} options.sessionId - Specific session ID to resume
|
|
27
|
+
* @returns {Promise<Object>} Execution result
|
|
28
|
+
*/
|
|
29
|
+
static async execute(profile, enhancedPrompt, options = {}) {
|
|
30
|
+
const {
|
|
31
|
+
cwd = process.cwd(),
|
|
32
|
+
timeout = 600000, // 10 minutes default
|
|
33
|
+
outputFormat = 'stream-json', // Use stream-json for real-time progress
|
|
34
|
+
permissionMode = 'acceptEdits',
|
|
35
|
+
resumeSession = false,
|
|
36
|
+
sessionId = null
|
|
37
|
+
} = options;
|
|
38
|
+
|
|
39
|
+
// Validate permission mode
|
|
40
|
+
this._validatePermissionMode(permissionMode);
|
|
41
|
+
|
|
42
|
+
// Initialize session manager
|
|
43
|
+
const sessionMgr = new SessionManager();
|
|
44
|
+
|
|
45
|
+
// Detect Claude CLI path
|
|
46
|
+
const claudeCli = this._detectClaudeCli();
|
|
47
|
+
if (!claudeCli) {
|
|
48
|
+
throw new Error('Claude CLI not found in PATH. Install from: https://docs.claude.com/en/docs/claude-code/installation');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get settings path for profile
|
|
52
|
+
const settingsPath = path.join(os.homedir(), '.ccs', `${profile}.settings.json`);
|
|
53
|
+
|
|
54
|
+
// Validate settings file exists
|
|
55
|
+
if (!fs.existsSync(settingsPath)) {
|
|
56
|
+
throw new Error(`Settings file not found: ${settingsPath}\nProfile "${profile}" may not be configured.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Smart slash command detection and preservation
|
|
60
|
+
// Detects if prompt contains slash command and restructures for proper execution
|
|
61
|
+
const processedPrompt = this._processSlashCommand(enhancedPrompt);
|
|
62
|
+
|
|
63
|
+
// Prepare arguments
|
|
64
|
+
const args = ['-p', processedPrompt, '--settings', settingsPath];
|
|
65
|
+
|
|
66
|
+
// Always use stream-json for real-time progress visibility
|
|
67
|
+
// Note: --verbose is required when using --print with stream-json
|
|
68
|
+
args.push('--output-format', 'stream-json', '--verbose');
|
|
69
|
+
|
|
70
|
+
// Add permission mode
|
|
71
|
+
if (permissionMode && permissionMode !== 'default') {
|
|
72
|
+
if (permissionMode === 'bypassPermissions') {
|
|
73
|
+
args.push('--dangerously-skip-permissions');
|
|
74
|
+
// Warn about dangerous mode
|
|
75
|
+
if (process.env.CCS_DEBUG) {
|
|
76
|
+
console.warn('[!] WARNING: Using --dangerously-skip-permissions mode');
|
|
77
|
+
console.warn('[!] This bypasses ALL permission checks. Use only in trusted environments.');
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
args.push('--permission-mode', permissionMode);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Add resume flag for multi-turn sessions
|
|
85
|
+
if (resumeSession) {
|
|
86
|
+
const lastSession = sessionMgr.getLastSession(profile);
|
|
87
|
+
|
|
88
|
+
if (lastSession) {
|
|
89
|
+
args.push('--resume', lastSession.sessionId);
|
|
90
|
+
if (process.env.CCS_DEBUG) {
|
|
91
|
+
console.error(`[i] Resuming session: ${lastSession.sessionId} (${lastSession.turns} turns, $${lastSession.totalCost.toFixed(4)})`);
|
|
92
|
+
}
|
|
93
|
+
} else if (sessionId) {
|
|
94
|
+
args.push('--resume', sessionId);
|
|
95
|
+
if (process.env.CCS_DEBUG) {
|
|
96
|
+
console.error(`[i] Resuming specific session: ${sessionId}`);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
console.warn('[!] No previous session found, starting new session');
|
|
100
|
+
}
|
|
101
|
+
} else if (sessionId) {
|
|
102
|
+
args.push('--resume', sessionId);
|
|
103
|
+
if (process.env.CCS_DEBUG) {
|
|
104
|
+
console.error(`[i] Resuming specific session: ${sessionId}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Add tool restrictions from settings
|
|
109
|
+
const toolRestrictions = SettingsParser.parseToolRestrictions(cwd);
|
|
110
|
+
|
|
111
|
+
if (toolRestrictions.allowedTools.length > 0) {
|
|
112
|
+
args.push('--allowedTools');
|
|
113
|
+
toolRestrictions.allowedTools.forEach(tool => args.push(tool));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (toolRestrictions.disallowedTools.length > 0) {
|
|
117
|
+
args.push('--disallowedTools');
|
|
118
|
+
toolRestrictions.disallowedTools.forEach(tool => args.push(tool));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Note: No max-turns limit - using time-based limits instead (default 10min timeout)
|
|
122
|
+
|
|
123
|
+
// Debug log args
|
|
124
|
+
if (process.env.CCS_DEBUG) {
|
|
125
|
+
console.error(`[i] Claude CLI args: ${args.join(' ')}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Execute with spawn
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const startTime = Date.now();
|
|
131
|
+
|
|
132
|
+
// Show progress unless explicitly disabled with CCS_QUIET
|
|
133
|
+
const showProgress = !process.env.CCS_QUIET;
|
|
134
|
+
|
|
135
|
+
// Show initial progress message
|
|
136
|
+
if (showProgress) {
|
|
137
|
+
const modelName = profile === 'glm' ? 'GLM-4.6' : profile === 'kimi' ? 'Kimi' : profile.toUpperCase();
|
|
138
|
+
console.error(`[i] Delegating to ${modelName}...`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const proc = spawn(claudeCli, args, {
|
|
142
|
+
cwd,
|
|
143
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
144
|
+
timeout
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
let stdout = '';
|
|
148
|
+
let stderr = '';
|
|
149
|
+
let progressInterval;
|
|
150
|
+
const messages = []; // Accumulate stream-json messages
|
|
151
|
+
let partialLine = ''; // Buffer for incomplete JSON lines
|
|
152
|
+
|
|
153
|
+
// Handle parent process termination (Ctrl+C or Esc in Claude)
|
|
154
|
+
// When main Claude session is killed, cleanup spawned child process
|
|
155
|
+
const cleanupHandler = () => {
|
|
156
|
+
if (!proc.killed) {
|
|
157
|
+
if (process.env.CCS_DEBUG) {
|
|
158
|
+
console.error('[!] Parent process terminating, killing delegated session...');
|
|
159
|
+
}
|
|
160
|
+
proc.kill('SIGTERM');
|
|
161
|
+
// Force kill if not dead after 2s
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
if (!proc.killed) {
|
|
164
|
+
proc.kill('SIGKILL');
|
|
165
|
+
}
|
|
166
|
+
}, 2000);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Register signal handlers for parent process termination
|
|
171
|
+
process.once('SIGINT', cleanupHandler);
|
|
172
|
+
process.once('SIGTERM', cleanupHandler);
|
|
173
|
+
|
|
174
|
+
// Cleanup signal handlers when child process exits
|
|
175
|
+
const removeSignalHandlers = () => {
|
|
176
|
+
process.removeListener('SIGINT', cleanupHandler);
|
|
177
|
+
process.removeListener('SIGTERM', cleanupHandler);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
proc.on('close', removeSignalHandlers);
|
|
181
|
+
proc.on('error', removeSignalHandlers);
|
|
182
|
+
|
|
183
|
+
// Progress indicator (show elapsed time every 5 seconds)
|
|
184
|
+
if (showProgress) {
|
|
185
|
+
progressInterval = setInterval(() => {
|
|
186
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
187
|
+
process.stderr.write(`[i] Still running... ${elapsed}s elapsed\r`);
|
|
188
|
+
}, 5000);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Capture stdout (stream-json format - jsonl)
|
|
192
|
+
proc.stdout.on('data', (data) => {
|
|
193
|
+
stdout += data.toString();
|
|
194
|
+
|
|
195
|
+
// Parse stream-json messages (jsonl format - one JSON per line)
|
|
196
|
+
const chunk = partialLine + data.toString();
|
|
197
|
+
const lines = chunk.split('\n');
|
|
198
|
+
partialLine = lines.pop() || ''; // Save incomplete line for next chunk
|
|
199
|
+
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
if (!line.trim()) continue;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const msg = JSON.parse(line);
|
|
205
|
+
messages.push(msg);
|
|
206
|
+
|
|
207
|
+
// Show real-time tool use with verbose details
|
|
208
|
+
if (showProgress && msg.type === 'assistant') {
|
|
209
|
+
const toolUses = msg.message?.content?.filter(c => c.type === 'tool_use') || [];
|
|
210
|
+
|
|
211
|
+
for (const tool of toolUses) {
|
|
212
|
+
process.stderr.write('\r\x1b[K'); // Clear line
|
|
213
|
+
|
|
214
|
+
// Show verbose tool use with description/input if available
|
|
215
|
+
const toolInput = tool.input || {};
|
|
216
|
+
let verboseMsg = `[Tool] ${tool.name}`;
|
|
217
|
+
|
|
218
|
+
// Add context based on tool type (all Claude Code tools)
|
|
219
|
+
switch (tool.name) {
|
|
220
|
+
case 'Bash':
|
|
221
|
+
if (toolInput.command) {
|
|
222
|
+
// Truncate long commands
|
|
223
|
+
const cmd = toolInput.command.length > 80
|
|
224
|
+
? toolInput.command.substring(0, 77) + '...'
|
|
225
|
+
: toolInput.command;
|
|
226
|
+
verboseMsg += `: ${cmd}`;
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
|
|
230
|
+
case 'Edit':
|
|
231
|
+
case 'Write':
|
|
232
|
+
case 'Read':
|
|
233
|
+
if (toolInput.file_path) {
|
|
234
|
+
verboseMsg += `: ${toolInput.file_path}`;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
|
|
238
|
+
case 'NotebookEdit':
|
|
239
|
+
case 'NotebookRead':
|
|
240
|
+
if (toolInput.notebook_path) {
|
|
241
|
+
verboseMsg += `: ${toolInput.notebook_path}`;
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case 'Grep':
|
|
246
|
+
if (toolInput.pattern) {
|
|
247
|
+
verboseMsg += `: searching for "${toolInput.pattern}"`;
|
|
248
|
+
if (toolInput.path) {
|
|
249
|
+
verboseMsg += ` in ${toolInput.path}`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
|
|
254
|
+
case 'Glob':
|
|
255
|
+
if (toolInput.pattern) {
|
|
256
|
+
verboseMsg += `: ${toolInput.pattern}`;
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
|
|
260
|
+
case 'SlashCommand':
|
|
261
|
+
if (toolInput.command) {
|
|
262
|
+
verboseMsg += `: ${toolInput.command}`;
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
case 'Task':
|
|
267
|
+
if (toolInput.description) {
|
|
268
|
+
verboseMsg += `: ${toolInput.description}`;
|
|
269
|
+
} else if (toolInput.prompt) {
|
|
270
|
+
const prompt = toolInput.prompt.length > 60
|
|
271
|
+
? toolInput.prompt.substring(0, 57) + '...'
|
|
272
|
+
: toolInput.prompt;
|
|
273
|
+
verboseMsg += `: ${prompt}`;
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
276
|
+
|
|
277
|
+
case 'TodoWrite':
|
|
278
|
+
if (toolInput.todos && Array.isArray(toolInput.todos)) {
|
|
279
|
+
// Show in_progress task instead of just count
|
|
280
|
+
const inProgressTask = toolInput.todos.find(t => t.status === 'in_progress');
|
|
281
|
+
if (inProgressTask && inProgressTask.activeForm) {
|
|
282
|
+
verboseMsg += `: ${inProgressTask.activeForm}`;
|
|
283
|
+
} else {
|
|
284
|
+
// Fallback to count if no in_progress task
|
|
285
|
+
verboseMsg += `: ${toolInput.todos.length} task(s)`;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
|
|
290
|
+
case 'WebFetch':
|
|
291
|
+
if (toolInput.url) {
|
|
292
|
+
verboseMsg += `: ${toolInput.url}`;
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
case 'WebSearch':
|
|
297
|
+
if (toolInput.query) {
|
|
298
|
+
verboseMsg += `: "${toolInput.query}"`;
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
default:
|
|
303
|
+
// For unknown tools, show first meaningful parameter
|
|
304
|
+
if (Object.keys(toolInput).length > 0) {
|
|
305
|
+
const firstKey = Object.keys(toolInput)[0];
|
|
306
|
+
const firstValue = toolInput[firstKey];
|
|
307
|
+
if (typeof firstValue === 'string' && firstValue.length < 60) {
|
|
308
|
+
verboseMsg += `: ${firstValue}`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
process.stderr.write(`${verboseMsg}\n`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch (parseError) {
|
|
317
|
+
// Skip malformed JSON lines (shouldn't happen with stream-json)
|
|
318
|
+
if (process.env.CCS_DEBUG) {
|
|
319
|
+
console.error(`[!] Failed to parse stream-json line: ${parseError.message}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Stream stderr in real-time (progress messages from Claude CLI)
|
|
326
|
+
proc.stderr.on('data', (data) => {
|
|
327
|
+
const stderrText = data.toString();
|
|
328
|
+
stderr += stderrText;
|
|
329
|
+
|
|
330
|
+
// Show stderr in real-time if in TTY
|
|
331
|
+
if (showProgress) {
|
|
332
|
+
// Clear progress line before showing stderr
|
|
333
|
+
if (progressInterval) {
|
|
334
|
+
process.stderr.write('\r\x1b[K'); // Clear line
|
|
335
|
+
}
|
|
336
|
+
process.stderr.write(stderrText);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Handle completion
|
|
341
|
+
proc.on('close', (exitCode) => {
|
|
342
|
+
const duration = Date.now() - startTime;
|
|
343
|
+
|
|
344
|
+
// Clear progress indicator
|
|
345
|
+
if (progressInterval) {
|
|
346
|
+
clearInterval(progressInterval);
|
|
347
|
+
process.stderr.write('\r\x1b[K'); // Clear line
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Show completion message
|
|
351
|
+
if (showProgress) {
|
|
352
|
+
const durationSec = (duration / 1000).toFixed(1);
|
|
353
|
+
if (timedOut) {
|
|
354
|
+
console.error(`[i] Execution timed out after ${durationSec}s`);
|
|
355
|
+
} else {
|
|
356
|
+
console.error(`[i] Execution completed in ${durationSec}s`);
|
|
357
|
+
}
|
|
358
|
+
console.error(''); // Blank line before formatted output
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const result = {
|
|
362
|
+
exitCode,
|
|
363
|
+
stdout,
|
|
364
|
+
stderr,
|
|
365
|
+
cwd,
|
|
366
|
+
profile,
|
|
367
|
+
duration,
|
|
368
|
+
timedOut,
|
|
369
|
+
success: exitCode === 0 && !timedOut,
|
|
370
|
+
messages // Include all stream-json messages
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Extract metadata from final 'result' message in stream-json
|
|
374
|
+
const resultMessage = messages.find(m => m.type === 'result');
|
|
375
|
+
if (resultMessage) {
|
|
376
|
+
// Add parsed fields from result message
|
|
377
|
+
result.sessionId = resultMessage.session_id || null;
|
|
378
|
+
result.totalCost = resultMessage.total_cost_usd || 0;
|
|
379
|
+
result.numTurns = resultMessage.num_turns || 0;
|
|
380
|
+
result.isError = resultMessage.is_error || false;
|
|
381
|
+
result.type = resultMessage.type || null;
|
|
382
|
+
result.subtype = resultMessage.subtype || null;
|
|
383
|
+
result.durationApi = resultMessage.duration_api_ms || 0;
|
|
384
|
+
result.permissionDenials = resultMessage.permission_denials || [];
|
|
385
|
+
result.errors = resultMessage.errors || [];
|
|
386
|
+
|
|
387
|
+
// Extract content from result message
|
|
388
|
+
result.content = resultMessage.result || '';
|
|
389
|
+
} else {
|
|
390
|
+
// Fallback: no result message found (shouldn't happen)
|
|
391
|
+
result.content = stdout;
|
|
392
|
+
if (process.env.CCS_DEBUG) {
|
|
393
|
+
console.error(`[!] No result message found in stream-json output`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Store or update session if we have session ID (even on timeout, for :continue support)
|
|
398
|
+
if (result.sessionId) {
|
|
399
|
+
if (resumeSession || sessionId) {
|
|
400
|
+
// Update existing session
|
|
401
|
+
sessionMgr.updateSession(profile, result.sessionId, {
|
|
402
|
+
totalCost: result.totalCost
|
|
403
|
+
});
|
|
404
|
+
} else {
|
|
405
|
+
// Store new session
|
|
406
|
+
sessionMgr.storeSession(profile, {
|
|
407
|
+
sessionId: result.sessionId,
|
|
408
|
+
totalCost: result.totalCost,
|
|
409
|
+
cwd: result.cwd
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Cleanup expired sessions periodically
|
|
414
|
+
if (Math.random() < 0.1) { // 10% chance
|
|
415
|
+
sessionMgr.cleanupExpired();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
resolve(result);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Handle errors
|
|
423
|
+
proc.on('error', (error) => {
|
|
424
|
+
if (progressInterval) {
|
|
425
|
+
clearInterval(progressInterval);
|
|
426
|
+
}
|
|
427
|
+
reject(new Error(`Failed to execute Claude CLI: ${error.message}`));
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Handle timeout with graceful SIGTERM then forceful SIGKILL
|
|
431
|
+
let timedOut = false;
|
|
432
|
+
if (timeout > 0) {
|
|
433
|
+
const timeoutHandle = setTimeout(() => {
|
|
434
|
+
if (!proc.killed) {
|
|
435
|
+
timedOut = true;
|
|
436
|
+
|
|
437
|
+
if (progressInterval) {
|
|
438
|
+
clearInterval(progressInterval);
|
|
439
|
+
process.stderr.write('\r\x1b[K'); // Clear line
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (process.env.CCS_DEBUG) {
|
|
443
|
+
console.error(`[!] Timeout reached after ${timeout}ms, sending SIGTERM for graceful shutdown...`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Send SIGTERM for graceful shutdown
|
|
447
|
+
proc.kill('SIGTERM');
|
|
448
|
+
|
|
449
|
+
// If process doesn't terminate within 10s, force kill
|
|
450
|
+
setTimeout(() => {
|
|
451
|
+
if (!proc.killed) {
|
|
452
|
+
if (process.env.CCS_DEBUG) {
|
|
453
|
+
console.error(`[!] Process did not terminate gracefully, sending SIGKILL...`);
|
|
454
|
+
}
|
|
455
|
+
proc.kill('SIGKILL');
|
|
456
|
+
}
|
|
457
|
+
}, 10000); // Give 10s for graceful shutdown instead of 5s
|
|
458
|
+
}
|
|
459
|
+
}, timeout);
|
|
460
|
+
|
|
461
|
+
// Clear timeout on successful completion
|
|
462
|
+
proc.on('close', () => clearTimeout(timeoutHandle));
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Validate permission mode
|
|
469
|
+
* @param {string} mode - Permission mode
|
|
470
|
+
* @throws {Error} If mode is invalid
|
|
471
|
+
* @private
|
|
472
|
+
*/
|
|
473
|
+
static _validatePermissionMode(mode) {
|
|
474
|
+
const VALID_MODES = ['default', 'plan', 'acceptEdits', 'bypassPermissions'];
|
|
475
|
+
if (!VALID_MODES.includes(mode)) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
`Invalid permission mode: "${mode}". Valid modes: ${VALID_MODES.join(', ')}`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Detect Claude CLI executable
|
|
484
|
+
* @returns {string|null} Path to claude CLI or null if not found
|
|
485
|
+
* @private
|
|
486
|
+
*/
|
|
487
|
+
static _detectClaudeCli() {
|
|
488
|
+
// Check environment variable override
|
|
489
|
+
if (process.env.CCS_CLAUDE_PATH) {
|
|
490
|
+
return process.env.CCS_CLAUDE_PATH;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Try to find in PATH
|
|
494
|
+
const { execSync } = require('child_process');
|
|
495
|
+
try {
|
|
496
|
+
const result = execSync('command -v claude', { encoding: 'utf8' });
|
|
497
|
+
return result.trim();
|
|
498
|
+
} catch (error) {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Execute with retry logic
|
|
505
|
+
* @param {string} profile - Profile name
|
|
506
|
+
* @param {string} enhancedPrompt - Enhanced prompt
|
|
507
|
+
* @param {Object} options - Execution options
|
|
508
|
+
* @param {number} options.maxRetries - Maximum retry attempts (default: 2)
|
|
509
|
+
* @returns {Promise<Object>} Execution result
|
|
510
|
+
*/
|
|
511
|
+
static async executeWithRetry(profile, enhancedPrompt, options = {}) {
|
|
512
|
+
const { maxRetries = 2, ...execOptions } = options;
|
|
513
|
+
let lastError;
|
|
514
|
+
|
|
515
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
516
|
+
try {
|
|
517
|
+
const result = await this.execute(profile, enhancedPrompt, execOptions);
|
|
518
|
+
|
|
519
|
+
// If successful, return immediately
|
|
520
|
+
if (result.success) {
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// If not last attempt, retry
|
|
525
|
+
if (attempt < maxRetries) {
|
|
526
|
+
console.error(`[!] Attempt ${attempt + 1} failed, retrying...`);
|
|
527
|
+
await this._sleep(1000 * (attempt + 1)); // Exponential backoff
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Last attempt failed, return result anyway
|
|
532
|
+
return result;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
lastError = error;
|
|
535
|
+
|
|
536
|
+
if (attempt < maxRetries) {
|
|
537
|
+
console.error(`[!] Attempt ${attempt + 1} errored: ${error.message}, retrying...`);
|
|
538
|
+
await this._sleep(1000 * (attempt + 1));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// All retries exhausted
|
|
544
|
+
throw lastError || new Error('Execution failed after all retry attempts');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Sleep utility for retry backoff
|
|
549
|
+
* @param {number} ms - Milliseconds to sleep
|
|
550
|
+
* @returns {Promise<void>}
|
|
551
|
+
* @private
|
|
552
|
+
*/
|
|
553
|
+
static _sleep(ms) {
|
|
554
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Process prompt to detect and preserve slash commands
|
|
559
|
+
* Implements smart enhancement: preserves slash command at start, allows context in rest
|
|
560
|
+
* @param {string} prompt - Original prompt (may contain slash command)
|
|
561
|
+
* @returns {string} Processed prompt with slash command preserved
|
|
562
|
+
* @private
|
|
563
|
+
*/
|
|
564
|
+
static _processSlashCommand(prompt) {
|
|
565
|
+
const trimmed = prompt.trim();
|
|
566
|
+
|
|
567
|
+
// Case 1: Already starts with slash command - keep as-is
|
|
568
|
+
if (trimmed.match(/^\/[\w:-]+(\s|$)/)) {
|
|
569
|
+
return prompt;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Case 2: Find slash command embedded in text
|
|
573
|
+
// Look for /command that's NOT part of a file path
|
|
574
|
+
// File paths: /home/user, /path/to/file (have / before or after)
|
|
575
|
+
// Commands: /cook, /plan (standalone, preceded by space/colon/start)
|
|
576
|
+
// Strategy: Find LAST occurrence that looks like a command, not a path
|
|
577
|
+
const embeddedSlash = trimmed.match(/(?:^|[^\w/])(\/[\w:-]+)(\s+[\s\S]*)?$/);
|
|
578
|
+
|
|
579
|
+
if (embeddedSlash) {
|
|
580
|
+
const command = embeddedSlash[1]; // e.g., "/cook"
|
|
581
|
+
const args = (embeddedSlash[2] || '').trim(); // Everything after command
|
|
582
|
+
|
|
583
|
+
// Calculate where the command starts (excluding preceding char if any)
|
|
584
|
+
const matchStart = embeddedSlash.index + (embeddedSlash[0][0] === '/' ? 0 : 1);
|
|
585
|
+
const beforeCommand = trimmed.substring(0, matchStart).trim();
|
|
586
|
+
|
|
587
|
+
// Restructure: command first, context after
|
|
588
|
+
if (beforeCommand && args) {
|
|
589
|
+
return `${command} ${args}\n\nContext: ${beforeCommand}`;
|
|
590
|
+
} else if (beforeCommand) {
|
|
591
|
+
return `${command}\n\nContext: ${beforeCommand}`;
|
|
592
|
+
}
|
|
593
|
+
return args ? `${command} ${args}` : command;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// No slash command detected, return as-is
|
|
597
|
+
return prompt;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Test if profile is executable (quick health check)
|
|
602
|
+
* @param {string} profile - Profile name
|
|
603
|
+
* @returns {Promise<boolean>} True if profile can execute
|
|
604
|
+
*/
|
|
605
|
+
static async testProfile(profile) {
|
|
606
|
+
try {
|
|
607
|
+
const result = await this.execute(profile, 'Say "test successful"', {
|
|
608
|
+
timeout: 10000
|
|
609
|
+
});
|
|
610
|
+
return result.success;
|
|
611
|
+
} catch (error) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
module.exports = { HeadlessExecutor };
|