@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.
Files changed (47) hide show
  1. package/.claude/agents/ccs-delegator.md +117 -0
  2. package/.claude/commands/ccs/glm/continue.md +22 -0
  3. package/.claude/commands/ccs/glm.md +22 -0
  4. package/.claude/commands/ccs/kimi/continue.md +22 -0
  5. package/.claude/commands/ccs/kimi.md +22 -0
  6. package/.claude/skills/ccs-delegation/SKILL.md +54 -0
  7. package/.claude/skills/ccs-delegation/references/README.md +24 -0
  8. package/.claude/skills/ccs-delegation/references/delegation-guidelines.md +99 -0
  9. package/.claude/skills/ccs-delegation/references/headless-workflow.md +174 -0
  10. package/.claude/skills/ccs-delegation/references/troubleshooting.md +268 -0
  11. package/README.ja.md +470 -146
  12. package/README.md +532 -145
  13. package/README.vi.md +484 -157
  14. package/VERSION +1 -1
  15. package/bin/auth/auth-commands.js +98 -13
  16. package/bin/auth/profile-detector.js +11 -6
  17. package/bin/ccs.js +148 -2
  18. package/bin/delegation/README.md +189 -0
  19. package/bin/delegation/delegation-handler.js +212 -0
  20. package/bin/delegation/headless-executor.js +617 -0
  21. package/bin/delegation/result-formatter.js +483 -0
  22. package/bin/delegation/session-manager.js +156 -0
  23. package/bin/delegation/settings-parser.js +109 -0
  24. package/bin/management/doctor.js +94 -1
  25. package/bin/utils/claude-symlink-manager.js +238 -0
  26. package/bin/utils/delegation-validator.js +154 -0
  27. package/bin/utils/error-codes.js +59 -0
  28. package/bin/utils/error-manager.js +38 -32
  29. package/bin/utils/helpers.js +65 -1
  30. package/bin/utils/progress-indicator.js +111 -0
  31. package/bin/utils/prompt.js +134 -0
  32. package/bin/utils/shell-completion.js +234 -0
  33. package/lib/ccs +575 -25
  34. package/lib/ccs.ps1 +381 -20
  35. package/lib/error-codes.ps1 +55 -0
  36. package/lib/error-codes.sh +63 -0
  37. package/lib/progress-indicator.ps1 +120 -0
  38. package/lib/progress-indicator.sh +117 -0
  39. package/lib/prompt.ps1 +109 -0
  40. package/lib/prompt.sh +99 -0
  41. package/package.json +2 -1
  42. package/scripts/completion/README.md +308 -0
  43. package/scripts/completion/ccs.bash +81 -0
  44. package/scripts/completion/ccs.fish +92 -0
  45. package/scripts/completion/ccs.ps1 +157 -0
  46. package/scripts/completion/ccs.zsh +130 -0
  47. 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 };