@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,483 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Formats delegation execution results for display
|
|
8
|
+
* Creates ASCII box output with file change tracking
|
|
9
|
+
*/
|
|
10
|
+
class ResultFormatter {
|
|
11
|
+
/**
|
|
12
|
+
* Format execution result with complete source-of-truth
|
|
13
|
+
* @param {Object} result - Execution result from HeadlessExecutor
|
|
14
|
+
* @param {string} result.profile - Profile used (glm, kimi, etc.)
|
|
15
|
+
* @param {string} result.cwd - Working directory
|
|
16
|
+
* @param {number} result.exitCode - Exit code
|
|
17
|
+
* @param {string} result.stdout - Standard output
|
|
18
|
+
* @param {string} result.stderr - Standard error
|
|
19
|
+
* @param {number} result.duration - Duration in milliseconds
|
|
20
|
+
* @param {boolean} result.success - Success flag
|
|
21
|
+
* @param {string} result.content - Parsed content (from JSON or stdout)
|
|
22
|
+
* @param {string} result.sessionId - Session ID (from JSON)
|
|
23
|
+
* @param {number} result.totalCost - Total cost USD (from JSON)
|
|
24
|
+
* @param {number} result.numTurns - Number of turns (from JSON)
|
|
25
|
+
* @returns {string} Formatted result
|
|
26
|
+
*/
|
|
27
|
+
static format(result) {
|
|
28
|
+
const { profile, cwd, exitCode, stdout, stderr, duration, success, content, sessionId, totalCost, numTurns, subtype, permissionDenials, errors, json, timedOut } = result;
|
|
29
|
+
|
|
30
|
+
// Handle timeout (graceful termination)
|
|
31
|
+
if (timedOut) {
|
|
32
|
+
return this._formatTimeoutError(result);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Handle legacy max_turns error (Claude CLI might still return this)
|
|
36
|
+
if (subtype === 'error_max_turns') {
|
|
37
|
+
return this._formatTimeoutError(result);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Use content field for output (JSON result or fallback stdout)
|
|
41
|
+
const displayOutput = content || stdout;
|
|
42
|
+
|
|
43
|
+
// Build formatted output
|
|
44
|
+
let output = '';
|
|
45
|
+
|
|
46
|
+
// Header
|
|
47
|
+
output += this._formatHeader(profile, success);
|
|
48
|
+
|
|
49
|
+
// Info box (file detection handled by delegated session itself)
|
|
50
|
+
output += this._formatInfoBox(cwd, profile, duration, exitCode, sessionId, totalCost, numTurns);
|
|
51
|
+
|
|
52
|
+
// Task output
|
|
53
|
+
output += '\n';
|
|
54
|
+
output += this._formatOutput(displayOutput);
|
|
55
|
+
|
|
56
|
+
// Permission denials if present
|
|
57
|
+
if (permissionDenials && permissionDenials.length > 0) {
|
|
58
|
+
output += '\n';
|
|
59
|
+
output += this._formatPermissionDenials(permissionDenials);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Errors if present
|
|
63
|
+
if (errors && errors.length > 0) {
|
|
64
|
+
output += '\n';
|
|
65
|
+
output += this._formatErrors(errors);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Stderr if present
|
|
69
|
+
if (stderr && stderr.trim()) {
|
|
70
|
+
output += '\n';
|
|
71
|
+
output += this._formatStderr(stderr);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Footer
|
|
75
|
+
output += '\n';
|
|
76
|
+
output += this._formatFooter(success, duration);
|
|
77
|
+
|
|
78
|
+
return output;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extract file changes from output
|
|
83
|
+
* @param {string} output - Command output
|
|
84
|
+
* @param {string} cwd - Working directory for filesystem scanning fallback
|
|
85
|
+
* @returns {Object} { created: Array<string>, modified: Array<string> }
|
|
86
|
+
*/
|
|
87
|
+
static extractFileChanges(output, cwd) {
|
|
88
|
+
const created = [];
|
|
89
|
+
const modified = [];
|
|
90
|
+
|
|
91
|
+
// Patterns to match file operations (case-insensitive)
|
|
92
|
+
const createdPatterns = [
|
|
93
|
+
/created:\s*([^\n\r]+)/gi,
|
|
94
|
+
/create:\s*([^\n\r]+)/gi,
|
|
95
|
+
/wrote:\s*([^\n\r]+)/gi,
|
|
96
|
+
/write:\s*([^\n\r]+)/gi,
|
|
97
|
+
/new file:\s*([^\n\r]+)/gi,
|
|
98
|
+
/generated:\s*([^\n\r]+)/gi,
|
|
99
|
+
/added:\s*([^\n\r]+)/gi
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const modifiedPatterns = [
|
|
103
|
+
/modified:\s*([^\n\r]+)/gi,
|
|
104
|
+
/update:\s*([^\n\r]+)/gi,
|
|
105
|
+
/updated:\s*([^\n\r]+)/gi,
|
|
106
|
+
/edit:\s*([^\n\r]+)/gi,
|
|
107
|
+
/edited:\s*([^\n\r]+)/gi,
|
|
108
|
+
/changed:\s*([^\n\r]+)/gi
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// Helper to check if file is infrastructure (should be ignored)
|
|
112
|
+
const isInfrastructure = (filePath) => {
|
|
113
|
+
return filePath.includes('/.claude/') || filePath.startsWith('.claude/');
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Extract created files
|
|
117
|
+
for (const pattern of createdPatterns) {
|
|
118
|
+
let match;
|
|
119
|
+
while ((match = pattern.exec(output)) !== null) {
|
|
120
|
+
const filePath = match[1].trim();
|
|
121
|
+
if (filePath && !created.includes(filePath) && !isInfrastructure(filePath)) {
|
|
122
|
+
created.push(filePath);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Extract modified files
|
|
128
|
+
for (const pattern of modifiedPatterns) {
|
|
129
|
+
let match;
|
|
130
|
+
while ((match = pattern.exec(output)) !== null) {
|
|
131
|
+
const filePath = match[1].trim();
|
|
132
|
+
// Don't include if already in created list or is infrastructure
|
|
133
|
+
if (filePath && !modified.includes(filePath) && !created.includes(filePath) && !isInfrastructure(filePath)) {
|
|
134
|
+
modified.push(filePath);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Fallback: Scan filesystem for recently modified files (last 5 minutes)
|
|
140
|
+
if (created.length === 0 && modified.length === 0 && cwd) {
|
|
141
|
+
try {
|
|
142
|
+
const fs = require('fs');
|
|
143
|
+
const childProcess = require('child_process');
|
|
144
|
+
|
|
145
|
+
// Use find command to get recently modified files (excluding infrastructure)
|
|
146
|
+
const findCmd = `find . -type f -mmin -5 -not -path "./.git/*" -not -path "./node_modules/*" -not -path "./.claude/*" 2>/dev/null | head -20`;
|
|
147
|
+
const result = childProcess.execSync(findCmd, { cwd, encoding: 'utf8', timeout: 5000 });
|
|
148
|
+
|
|
149
|
+
const files = result.split('\n').filter(f => f.trim());
|
|
150
|
+
files.forEach(file => {
|
|
151
|
+
const fullPath = path.join(cwd, file);
|
|
152
|
+
|
|
153
|
+
// Double-check not infrastructure
|
|
154
|
+
if (isInfrastructure(fullPath)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const stats = fs.statSync(fullPath);
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
const mtime = stats.mtimeMs;
|
|
162
|
+
const ctime = stats.ctimeMs;
|
|
163
|
+
|
|
164
|
+
// If both mtime and ctime are very recent (within 10 minutes), likely created
|
|
165
|
+
// ctime = inode change time, for new files this is close to creation time
|
|
166
|
+
const isVeryRecent = (now - mtime) < 600000 && (now - ctime) < 600000;
|
|
167
|
+
const timeDiff = Math.abs(mtime - ctime);
|
|
168
|
+
|
|
169
|
+
// If mtime and ctime are very close (< 1 second apart) and both recent, it's created
|
|
170
|
+
if (isVeryRecent && timeDiff < 1000) {
|
|
171
|
+
if (!created.includes(fullPath)) {
|
|
172
|
+
created.push(fullPath);
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
// Otherwise, it's modified
|
|
176
|
+
if (!modified.includes(fullPath)) {
|
|
177
|
+
modified.push(fullPath);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} catch (statError) {
|
|
181
|
+
// If stat fails, default to created (since we're in fallback mode)
|
|
182
|
+
if (!created.includes(fullPath) && !modified.includes(fullPath)) {
|
|
183
|
+
created.push(fullPath);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
} catch (scanError) {
|
|
188
|
+
// Silently fail if filesystem scan doesn't work
|
|
189
|
+
if (process.env.CCS_DEBUG) {
|
|
190
|
+
console.error(`[!] Filesystem scan failed: ${scanError.message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { created, modified };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Format header with delegation indicator
|
|
200
|
+
* @param {string} profile - Profile name
|
|
201
|
+
* @param {boolean} success - Success flag
|
|
202
|
+
* @returns {string} Formatted header
|
|
203
|
+
* @private
|
|
204
|
+
*/
|
|
205
|
+
static _formatHeader(profile, success) {
|
|
206
|
+
const modelName = this._getModelDisplayName(profile);
|
|
207
|
+
const icon = success ? '[i]' : '[X]';
|
|
208
|
+
return `${icon} Delegated to ${modelName} (ccs:${profile})\n`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Format info box with delegation details
|
|
213
|
+
* @param {string} cwd - Working directory
|
|
214
|
+
* @param {string} profile - Profile name
|
|
215
|
+
* @param {number} duration - Duration in ms
|
|
216
|
+
* @param {number} exitCode - Exit code
|
|
217
|
+
* @param {string} sessionId - Session ID (from JSON)
|
|
218
|
+
* @param {number} totalCost - Total cost USD (from JSON)
|
|
219
|
+
* @param {number} numTurns - Number of turns (from JSON)
|
|
220
|
+
* @returns {string} Formatted info box
|
|
221
|
+
* @private
|
|
222
|
+
*/
|
|
223
|
+
static _formatInfoBox(cwd, profile, duration, exitCode, sessionId, totalCost, numTurns) {
|
|
224
|
+
const modelName = this._getModelDisplayName(profile);
|
|
225
|
+
const durationSec = (duration / 1000).toFixed(1);
|
|
226
|
+
|
|
227
|
+
// Calculate box width (fit longest line + padding)
|
|
228
|
+
const maxWidth = 70;
|
|
229
|
+
const cwdLine = `Working Directory: ${cwd}`;
|
|
230
|
+
const boxWidth = Math.min(Math.max(cwdLine.length + 4, 50), maxWidth);
|
|
231
|
+
|
|
232
|
+
const lines = [
|
|
233
|
+
`Working Directory: ${this._truncate(cwd, boxWidth - 22)}`,
|
|
234
|
+
`Model: ${modelName}`,
|
|
235
|
+
`Duration: ${durationSec}s`,
|
|
236
|
+
`Exit Code: ${exitCode}`
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
// Add JSON-specific fields if available
|
|
240
|
+
if (sessionId) {
|
|
241
|
+
// Abbreviate session ID (Git-style first 8 chars) to prevent wrapping
|
|
242
|
+
const shortId = sessionId.length > 8 ? sessionId.substring(0, 8) : sessionId;
|
|
243
|
+
lines.push(`Session ID: ${shortId}`);
|
|
244
|
+
}
|
|
245
|
+
if (totalCost !== undefined && totalCost !== null) {
|
|
246
|
+
lines.push(`Cost: $${totalCost.toFixed(4)}`);
|
|
247
|
+
}
|
|
248
|
+
if (numTurns) {
|
|
249
|
+
lines.push(`Turns: ${numTurns}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let box = '';
|
|
253
|
+
box += '╔' + '═'.repeat(boxWidth - 2) + '╗\n';
|
|
254
|
+
|
|
255
|
+
for (const line of lines) {
|
|
256
|
+
const padding = boxWidth - line.length - 4;
|
|
257
|
+
box += '║ ' + line + ' '.repeat(Math.max(0, padding)) + ' ║\n';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
box += '╚' + '═'.repeat(boxWidth - 2) + '╝';
|
|
261
|
+
|
|
262
|
+
return box;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Format task output
|
|
267
|
+
* @param {string} output - Standard output
|
|
268
|
+
* @returns {string} Formatted output
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
static _formatOutput(output) {
|
|
272
|
+
if (!output || !output.trim()) {
|
|
273
|
+
return '[i] No output from delegated task\n';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return output.trim() + '\n';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Format stderr output
|
|
281
|
+
* @param {string} stderr - Standard error
|
|
282
|
+
* @returns {string} Formatted stderr
|
|
283
|
+
* @private
|
|
284
|
+
*/
|
|
285
|
+
static _formatStderr(stderr) {
|
|
286
|
+
return `[!] Stderr:\n${stderr.trim()}\n\n`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Format file list (created or modified)
|
|
291
|
+
* @param {string} label - Label (Created/Modified)
|
|
292
|
+
* @param {Array<string>} files - File paths
|
|
293
|
+
* @returns {string} Formatted file list
|
|
294
|
+
* @private
|
|
295
|
+
*/
|
|
296
|
+
static _formatFileList(label, files) {
|
|
297
|
+
let output = `[i] ${label} Files:\n`;
|
|
298
|
+
|
|
299
|
+
for (const file of files) {
|
|
300
|
+
output += ` - ${file}\n`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return output;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Format footer with completion status
|
|
308
|
+
* @param {boolean} success - Success flag
|
|
309
|
+
* @param {number} duration - Duration in ms
|
|
310
|
+
* @returns {string} Formatted footer
|
|
311
|
+
* @private
|
|
312
|
+
*/
|
|
313
|
+
static _formatFooter(success, duration) {
|
|
314
|
+
const icon = success ? '[OK]' : '[X]';
|
|
315
|
+
const status = success ? 'Delegation completed' : 'Delegation failed';
|
|
316
|
+
return `${icon} ${status}\n`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get display name for model profile
|
|
321
|
+
* @param {string} profile - Profile name
|
|
322
|
+
* @returns {string} Display name
|
|
323
|
+
* @private
|
|
324
|
+
*/
|
|
325
|
+
static _getModelDisplayName(profile) {
|
|
326
|
+
const displayNames = {
|
|
327
|
+
'glm': 'GLM-4.6',
|
|
328
|
+
'glmt': 'GLM-4.6 (Thinking)',
|
|
329
|
+
'kimi': 'Kimi',
|
|
330
|
+
'default': 'Claude'
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
return displayNames[profile] || profile.toUpperCase();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Truncate string to max length
|
|
338
|
+
* @param {string} str - String to truncate
|
|
339
|
+
* @param {number} maxLength - Maximum length
|
|
340
|
+
* @returns {string} Truncated string
|
|
341
|
+
* @private
|
|
342
|
+
*/
|
|
343
|
+
static _truncate(str, maxLength) {
|
|
344
|
+
if (str.length <= maxLength) {
|
|
345
|
+
return str;
|
|
346
|
+
}
|
|
347
|
+
return str.substring(0, maxLength - 3) + '...';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Format minimal result (for quick tasks)
|
|
352
|
+
* @param {Object} result - Execution result
|
|
353
|
+
* @returns {string} Minimal formatted result
|
|
354
|
+
*/
|
|
355
|
+
static formatMinimal(result) {
|
|
356
|
+
const { profile, success, duration } = result;
|
|
357
|
+
const modelName = this._getModelDisplayName(profile);
|
|
358
|
+
const icon = success ? '[OK]' : '[X]';
|
|
359
|
+
const durationSec = (duration / 1000).toFixed(1);
|
|
360
|
+
|
|
361
|
+
return `${icon} ${modelName} delegation ${success ? 'completed' : 'failed'} (${durationSec}s)\n`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Format verbose result (with full details)
|
|
366
|
+
* @param {Object} result - Execution result
|
|
367
|
+
* @returns {string} Verbose formatted result
|
|
368
|
+
*/
|
|
369
|
+
static formatVerbose(result) {
|
|
370
|
+
const basic = this.format(result);
|
|
371
|
+
|
|
372
|
+
// Add additional debug info
|
|
373
|
+
let verbose = basic;
|
|
374
|
+
verbose += '\n=== Debug Information ===\n';
|
|
375
|
+
verbose += `CWD: ${result.cwd}\n`;
|
|
376
|
+
verbose += `Profile: ${result.profile}\n`;
|
|
377
|
+
verbose += `Exit Code: ${result.exitCode}\n`;
|
|
378
|
+
verbose += `Duration: ${result.duration}ms\n`;
|
|
379
|
+
verbose += `Success: ${result.success}\n`;
|
|
380
|
+
verbose += `Stdout Length: ${result.stdout.length} chars\n`;
|
|
381
|
+
verbose += `Stderr Length: ${result.stderr.length} chars\n`;
|
|
382
|
+
|
|
383
|
+
return verbose;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Check if NO_COLOR environment variable is set
|
|
388
|
+
* @returns {boolean} True if colors should be disabled
|
|
389
|
+
* @private
|
|
390
|
+
*/
|
|
391
|
+
static _shouldDisableColors() {
|
|
392
|
+
return process.env.NO_COLOR !== undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Format timeout error (session exceeded time limit)
|
|
397
|
+
* @param {Object} result - Execution result
|
|
398
|
+
* @returns {string} Formatted timeout error
|
|
399
|
+
* @private
|
|
400
|
+
*/
|
|
401
|
+
static _formatTimeoutError(result) {
|
|
402
|
+
const { profile, cwd, duration, sessionId, totalCost, numTurns, permissionDenials } = result;
|
|
403
|
+
|
|
404
|
+
let output = '';
|
|
405
|
+
|
|
406
|
+
// Header
|
|
407
|
+
output += this._formatHeader(profile, false);
|
|
408
|
+
|
|
409
|
+
// Info box
|
|
410
|
+
output += this._formatInfoBox(cwd, profile, duration, 0, sessionId, totalCost, numTurns);
|
|
411
|
+
|
|
412
|
+
// Timeout message
|
|
413
|
+
output += '\n';
|
|
414
|
+
const timeoutMin = (duration / 60000).toFixed(1);
|
|
415
|
+
output += `[!] Execution timed out after ${timeoutMin} minutes\n\n`;
|
|
416
|
+
output += 'The delegated session exceeded its time limit before completing the task.\n';
|
|
417
|
+
output += 'Session was gracefully terminated and saved for continuation.\n';
|
|
418
|
+
|
|
419
|
+
// Permission denials if present
|
|
420
|
+
if (permissionDenials && permissionDenials.length > 0) {
|
|
421
|
+
output += '\n';
|
|
422
|
+
output += this._formatPermissionDenials(permissionDenials);
|
|
423
|
+
output += '\n';
|
|
424
|
+
output += 'The task may require permissions that were denied.\n';
|
|
425
|
+
output += 'Consider running with --permission-mode bypassPermissions or execute manually.\n';
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Suggestions
|
|
429
|
+
output += '\n';
|
|
430
|
+
output += 'Suggestions:\n';
|
|
431
|
+
output += ` - Continue session: ccs ${profile}:continue -p "finish the task"\n`;
|
|
432
|
+
output += ` - Increase timeout: ccs ${profile} -p "task" --timeout ${duration * 2}\n`;
|
|
433
|
+
output += ' - Break task into smaller steps\n';
|
|
434
|
+
output += ' - Run task manually in main Claude session\n';
|
|
435
|
+
|
|
436
|
+
output += '\n';
|
|
437
|
+
// Abbreviate session ID (Git-style first 8 chars)
|
|
438
|
+
const shortId = sessionId && sessionId.length > 8 ? sessionId.substring(0, 8) : sessionId;
|
|
439
|
+
output += `[i] Session persisted with ID: ${shortId}\n`;
|
|
440
|
+
output += `[i] Cost: $${totalCost.toFixed(4)}\n`;
|
|
441
|
+
|
|
442
|
+
return output;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Format permission denials
|
|
447
|
+
* @param {Array<Object>} denials - Permission denial objects
|
|
448
|
+
* @returns {string} Formatted permission denials
|
|
449
|
+
* @private
|
|
450
|
+
*/
|
|
451
|
+
static _formatPermissionDenials(denials) {
|
|
452
|
+
let output = '[!] Permission Denials:\n';
|
|
453
|
+
|
|
454
|
+
for (const denial of denials) {
|
|
455
|
+
const tool = denial.tool_name || 'Unknown';
|
|
456
|
+
const input = denial.tool_input || {};
|
|
457
|
+
const command = input.command || input.description || JSON.stringify(input);
|
|
458
|
+
|
|
459
|
+
output += ` - ${tool}: ${command}\n`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return output;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Format errors array
|
|
467
|
+
* @param {Array<Object>} errors - Error objects
|
|
468
|
+
* @returns {string} Formatted errors
|
|
469
|
+
* @private
|
|
470
|
+
*/
|
|
471
|
+
static _formatErrors(errors) {
|
|
472
|
+
let output = '[X] Errors:\n';
|
|
473
|
+
|
|
474
|
+
for (const error of errors) {
|
|
475
|
+
const message = error.message || error.error || JSON.stringify(error);
|
|
476
|
+
output += ` - ${message}\n`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return output;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
module.exports = { ResultFormatter };
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
* Manages delegation session persistence for multi-turn conversations
|
|
10
|
+
*/
|
|
11
|
+
class SessionManager {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.sessionsPath = path.join(os.homedir(), '.ccs', 'delegation-sessions.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Store new session metadata
|
|
18
|
+
* @param {string} profile - Profile name (glm, kimi, etc.)
|
|
19
|
+
* @param {Object} sessionData - Session data
|
|
20
|
+
* @param {string} sessionData.sessionId - Claude session ID
|
|
21
|
+
* @param {number} sessionData.totalCost - Initial cost
|
|
22
|
+
* @param {string} sessionData.cwd - Working directory
|
|
23
|
+
*/
|
|
24
|
+
storeSession(profile, sessionData) {
|
|
25
|
+
const sessions = this._loadSessions();
|
|
26
|
+
const key = `${profile}:latest`;
|
|
27
|
+
|
|
28
|
+
sessions[key] = {
|
|
29
|
+
sessionId: sessionData.sessionId,
|
|
30
|
+
profile,
|
|
31
|
+
startTime: Date.now(),
|
|
32
|
+
lastTurnTime: Date.now(),
|
|
33
|
+
totalCost: sessionData.totalCost || 0,
|
|
34
|
+
turns: 1,
|
|
35
|
+
cwd: sessionData.cwd || process.cwd()
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
this._saveSessions(sessions);
|
|
39
|
+
|
|
40
|
+
if (process.env.CCS_DEBUG) {
|
|
41
|
+
console.error(`[i] Stored session: ${sessionData.sessionId} for ${profile}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Update session after additional turn
|
|
47
|
+
* @param {string} profile - Profile name
|
|
48
|
+
* @param {string} sessionId - Session ID
|
|
49
|
+
* @param {Object} turnData - Turn data
|
|
50
|
+
* @param {number} turnData.totalCost - Turn cost
|
|
51
|
+
*/
|
|
52
|
+
updateSession(profile, sessionId, turnData) {
|
|
53
|
+
const sessions = this._loadSessions();
|
|
54
|
+
const key = `${profile}:latest`;
|
|
55
|
+
|
|
56
|
+
if (sessions[key]?.sessionId === sessionId) {
|
|
57
|
+
sessions[key].lastTurnTime = Date.now();
|
|
58
|
+
sessions[key].totalCost += turnData.totalCost || 0;
|
|
59
|
+
sessions[key].turns += 1;
|
|
60
|
+
this._saveSessions(sessions);
|
|
61
|
+
|
|
62
|
+
if (process.env.CCS_DEBUG) {
|
|
63
|
+
console.error(`[i] Updated session: ${sessionId}, total: $${sessions[key].totalCost.toFixed(4)}, turns: ${sessions[key].turns}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get last session for profile
|
|
70
|
+
* @param {string} profile - Profile name
|
|
71
|
+
* @returns {Object|null} Session metadata or null
|
|
72
|
+
*/
|
|
73
|
+
getLastSession(profile) {
|
|
74
|
+
const sessions = this._loadSessions();
|
|
75
|
+
const key = `${profile}:latest`;
|
|
76
|
+
return sessions[key] || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Clear all sessions for profile
|
|
81
|
+
* @param {string} profile - Profile name
|
|
82
|
+
*/
|
|
83
|
+
clearProfile(profile) {
|
|
84
|
+
const sessions = this._loadSessions();
|
|
85
|
+
const key = `${profile}:latest`;
|
|
86
|
+
delete sessions[key];
|
|
87
|
+
this._saveSessions(sessions);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Clean up expired sessions (>30 days)
|
|
92
|
+
*/
|
|
93
|
+
cleanupExpired() {
|
|
94
|
+
const sessions = this._loadSessions();
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
97
|
+
|
|
98
|
+
let cleaned = 0;
|
|
99
|
+
Object.keys(sessions).forEach(key => {
|
|
100
|
+
if (now - sessions[key].lastTurnTime > maxAge) {
|
|
101
|
+
delete sessions[key];
|
|
102
|
+
cleaned++;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (cleaned > 0) {
|
|
107
|
+
this._saveSessions(sessions);
|
|
108
|
+
if (process.env.CCS_DEBUG) {
|
|
109
|
+
console.error(`[i] Cleaned ${cleaned} expired sessions`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Load sessions from disk
|
|
116
|
+
* @returns {Object} Sessions object
|
|
117
|
+
* @private
|
|
118
|
+
*/
|
|
119
|
+
_loadSessions() {
|
|
120
|
+
try {
|
|
121
|
+
if (!fs.existsSync(this.sessionsPath)) {
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
const content = fs.readFileSync(this.sessionsPath, 'utf8');
|
|
125
|
+
return JSON.parse(content);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (process.env.CCS_DEBUG) {
|
|
128
|
+
console.warn(`[!] Failed to load sessions: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Save sessions to disk
|
|
136
|
+
* @param {Object} sessions - Sessions object
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
_saveSessions(sessions) {
|
|
140
|
+
try {
|
|
141
|
+
const dir = path.dirname(this.sessionsPath);
|
|
142
|
+
if (!fs.existsSync(dir)) {
|
|
143
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
144
|
+
}
|
|
145
|
+
fs.writeFileSync(
|
|
146
|
+
this.sessionsPath,
|
|
147
|
+
JSON.stringify(sessions, null, 2),
|
|
148
|
+
{ mode: 0o600 }
|
|
149
|
+
);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error(`[!] Failed to save sessions: ${error.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { SessionManager };
|