cc-dev-template 0.1.1
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/bin/install.js +165 -0
- package/package.json +24 -0
- package/src/agents/claude-md-agent.md +71 -0
- package/src/agents/decomposition-agent.md +103 -0
- package/src/agents/execution-agent.md +133 -0
- package/src/agents/rca-agent.md +158 -0
- package/src/agents/tdd-agent.md +163 -0
- package/src/commands/finalize.md +83 -0
- package/src/commands/prime.md +5 -0
- package/src/scripts/adr-list.js +170 -0
- package/src/scripts/adr-tags.js +125 -0
- package/src/scripts/merge-settings.js +187 -0
- package/src/scripts/statusline-config.json +7 -0
- package/src/scripts/statusline.js +365 -0
- package/src/scripts/validate-yaml.js +128 -0
- package/src/scripts/yaml-validation-hook.json +15 -0
- package/src/skills/orchestration/SKILL.md +127 -0
- package/src/skills/orchestration/references/debugging/describe.md +122 -0
- package/src/skills/orchestration/references/debugging/fix.md +110 -0
- package/src/skills/orchestration/references/debugging/learn.md +162 -0
- package/src/skills/orchestration/references/debugging/rca.md +84 -0
- package/src/skills/orchestration/references/debugging/verify.md +95 -0
- package/src/skills/orchestration/references/execution/complete.md +161 -0
- package/src/skills/orchestration/references/execution/start.md +66 -0
- package/src/skills/orchestration/references/execution/tasks.md +92 -0
- package/src/skills/orchestration/references/planning/draft.md +195 -0
- package/src/skills/orchestration/references/planning/explore.md +129 -0
- package/src/skills/orchestration/references/planning/finalize.md +169 -0
- package/src/skills/orchestration/references/planning/start.md +115 -0
- package/src/skills/orchestration/scripts/plan-status.js +283 -0
- package/src/skills/prompting/SKILL.md +123 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* merge-settings.js - Merge configuration into ~/.claude/settings.json
|
|
5
|
+
*
|
|
6
|
+
* This utility script safely merges configuration into the Claude Code
|
|
7
|
+
* settings.json file, handling all edge cases:
|
|
8
|
+
* - File doesn't exist (creates it)
|
|
9
|
+
* - Preserves all existing configuration
|
|
10
|
+
* - Special handling for hooks.PostToolUse arrays (prevents duplicates)
|
|
11
|
+
* - Simple merge for other top-level properties
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node ~/.claude/scripts/merge-settings.js <settings-file> <config-file>
|
|
15
|
+
*
|
|
16
|
+
* Arguments:
|
|
17
|
+
* settings-file Path to the settings.json file (e.g., ~/.claude/settings.json)
|
|
18
|
+
* config-file Path to a JSON file containing the configuration to merge
|
|
19
|
+
*
|
|
20
|
+
* Config format examples:
|
|
21
|
+
*
|
|
22
|
+
* Hook configuration:
|
|
23
|
+
* {
|
|
24
|
+
* "hooks": {
|
|
25
|
+
* "PostToolUse": [
|
|
26
|
+
* {
|
|
27
|
+
* "matcher": "Edit|Write",
|
|
28
|
+
* "hooks": [{ "type": "command", "command": "..." }]
|
|
29
|
+
* }
|
|
30
|
+
* ]
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* Status line configuration:
|
|
35
|
+
* {
|
|
36
|
+
* "statusLine": {
|
|
37
|
+
* "type": "command",
|
|
38
|
+
* "command": "node ~/.claude/scripts/statusline.js"
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
*
|
|
42
|
+
* Exit codes:
|
|
43
|
+
* 0 = Success (settings merged or already correct)
|
|
44
|
+
* 1 = Error (file I/O or JSON parsing failed)
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
const path = require('path');
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extract command strings from a hooks array (handles both string and object formats)
|
|
52
|
+
* @param {Array} hooks - Array of hook definitions (strings or objects with command property)
|
|
53
|
+
* @returns {Set<string>} - Set of command strings
|
|
54
|
+
*/
|
|
55
|
+
function extractCommands(hooks) {
|
|
56
|
+
const commands = new Set();
|
|
57
|
+
if (!hooks) return commands;
|
|
58
|
+
|
|
59
|
+
for (const hook of hooks) {
|
|
60
|
+
if (typeof hook === 'string') {
|
|
61
|
+
commands.add(hook);
|
|
62
|
+
} else if (hook && typeof hook.command === 'string') {
|
|
63
|
+
commands.add(hook.command);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return commands;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Merge hooks configuration with duplicate prevention
|
|
71
|
+
* @param {object} existingHooks - The existing hooks object
|
|
72
|
+
* @param {object} newHooks - The new hooks to merge
|
|
73
|
+
* @returns {object} - The merged hooks object
|
|
74
|
+
*/
|
|
75
|
+
function mergeHooks(existingHooks, newHooks) {
|
|
76
|
+
const result = { ...existingHooks };
|
|
77
|
+
|
|
78
|
+
for (const [hookType, hookArray] of Object.entries(newHooks)) {
|
|
79
|
+
if (!Array.isArray(hookArray)) continue;
|
|
80
|
+
|
|
81
|
+
if (!result[hookType]) {
|
|
82
|
+
result[hookType] = [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// For each new hook, check if it already exists before adding
|
|
86
|
+
for (const newHook of hookArray) {
|
|
87
|
+
const newCommands = extractCommands(newHook.hooks);
|
|
88
|
+
|
|
89
|
+
const hookExists = result[hookType].some((existingHook) => {
|
|
90
|
+
if (existingHook.matcher !== newHook.matcher) return false;
|
|
91
|
+
|
|
92
|
+
const existingCommands = extractCommands(existingHook.hooks);
|
|
93
|
+
for (const cmd of newCommands) {
|
|
94
|
+
if (existingCommands.has(cmd)) return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!hookExists) {
|
|
100
|
+
result[hookType].push(newHook);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Merge configuration into existing settings
|
|
110
|
+
* @param {object} existing - The existing settings object
|
|
111
|
+
* @param {object} newConfig - The new configuration to merge
|
|
112
|
+
* @returns {object} - The merged settings object
|
|
113
|
+
*/
|
|
114
|
+
function mergeSettings(existing, newConfig) {
|
|
115
|
+
const result = { ...existing };
|
|
116
|
+
|
|
117
|
+
for (const [key, value] of Object.entries(newConfig)) {
|
|
118
|
+
if (key === 'hooks' && typeof value === 'object') {
|
|
119
|
+
// Special handling for hooks - merge arrays with duplicate prevention
|
|
120
|
+
result.hooks = mergeHooks(result.hooks || {}, value);
|
|
121
|
+
} else {
|
|
122
|
+
// For other properties, simply overwrite (or add if new)
|
|
123
|
+
result[key] = value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Main function to merge settings
|
|
132
|
+
*/
|
|
133
|
+
function main() {
|
|
134
|
+
const args = process.argv.slice(2);
|
|
135
|
+
|
|
136
|
+
if (args.length < 2) {
|
|
137
|
+
console.error('Usage: node merge-settings.js <settings-file> <config-file>');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const settingsPath = args[0];
|
|
142
|
+
const configPath = args[1];
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Read configuration to merge
|
|
146
|
+
let config;
|
|
147
|
+
try {
|
|
148
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
149
|
+
config = JSON.parse(configContent);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error(`Error reading config from ${configPath}: ${error.message}`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Read existing settings or create empty object
|
|
156
|
+
let settings = {};
|
|
157
|
+
if (fs.existsSync(settingsPath)) {
|
|
158
|
+
try {
|
|
159
|
+
const settingsContent = fs.readFileSync(settingsPath, 'utf8');
|
|
160
|
+
settings = JSON.parse(settingsContent);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(`Error parsing existing settings at ${settingsPath}: ${error.message}`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Merge the configurations
|
|
168
|
+
const merged = mergeSettings(settings, config);
|
|
169
|
+
|
|
170
|
+
// Ensure directory exists
|
|
171
|
+
const dir = path.dirname(settingsPath);
|
|
172
|
+
if (!fs.existsSync(dir)) {
|
|
173
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Write merged settings back to file
|
|
177
|
+
fs.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
178
|
+
|
|
179
|
+
console.log(`✓ Settings merged successfully: ${settingsPath}`);
|
|
180
|
+
process.exit(0);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error(`Error merging settings: ${error.message}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main();
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom Status Line for Claude Code
|
|
5
|
+
*
|
|
6
|
+
* Displays project status in lines:
|
|
7
|
+
* - Directory name + Git branch + status (combined)
|
|
8
|
+
* - Context window usage bar with percentage and tokens
|
|
9
|
+
*
|
|
10
|
+
* Input: JSON on stdin with transcript_path
|
|
11
|
+
* Output: Formatted status to stdout
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { existsSync, readFileSync, readdirSync, statSync } = require('fs');
|
|
15
|
+
const { join, basename } = require('path');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get model token limit based on model ID
|
|
20
|
+
*/
|
|
21
|
+
function getModelTokenLimit(modelId) {
|
|
22
|
+
// Claude model context windows
|
|
23
|
+
if (modelId.includes('claude-3-5-sonnet') || modelId.includes('claude-sonnet-4')) {
|
|
24
|
+
return 200000;
|
|
25
|
+
}
|
|
26
|
+
if (modelId.includes('claude-3-opus')) {
|
|
27
|
+
return 200000;
|
|
28
|
+
}
|
|
29
|
+
if (modelId.includes('claude-3-haiku')) {
|
|
30
|
+
return 200000;
|
|
31
|
+
}
|
|
32
|
+
// Default fallback
|
|
33
|
+
return 200000;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Calculate context window usage from transcript
|
|
38
|
+
* Uses MOST RECENT message only (each message already reports full context)
|
|
39
|
+
*/
|
|
40
|
+
function calculateTokenUsage(transcriptPath) {
|
|
41
|
+
try {
|
|
42
|
+
if (!existsSync(transcriptPath)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const content = readFileSync(transcriptPath, 'utf-8');
|
|
47
|
+
const lines = content.trim().split('\n');
|
|
48
|
+
|
|
49
|
+
// Parse all messages
|
|
50
|
+
const messages = [];
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
if (!line.trim()) continue;
|
|
53
|
+
try {
|
|
54
|
+
messages.push(JSON.parse(line));
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Find most recent main-chain message with usage data
|
|
61
|
+
const mainChainMessages = messages.filter((m) => m.isSidechain === false);
|
|
62
|
+
if (mainChainMessages.length === 0) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Sort by timestamp descending (most recent first)
|
|
67
|
+
mainChainMessages.sort(
|
|
68
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Find first message with usage data
|
|
72
|
+
let contextLength = 0;
|
|
73
|
+
let modelId = '';
|
|
74
|
+
|
|
75
|
+
for (const msg of mainChainMessages) {
|
|
76
|
+
if (msg.message?.usage) {
|
|
77
|
+
const usage = msg.message.usage;
|
|
78
|
+
// Context = input + cache_read + cache_creation
|
|
79
|
+
// (each message already reports FULL context at that point)
|
|
80
|
+
contextLength =
|
|
81
|
+
(usage.input_tokens || 0) +
|
|
82
|
+
(usage.cache_read_input_tokens || 0) +
|
|
83
|
+
(usage.cache_creation_input_tokens || 0);
|
|
84
|
+
modelId = msg.message.model || '';
|
|
85
|
+
break; // Stop after most recent usage!
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const limit = getModelTokenLimit(modelId);
|
|
90
|
+
const percentage = Math.min(100, Math.round((contextLength / limit) * 100));
|
|
91
|
+
|
|
92
|
+
return { used: contextLength, limit, percentage };
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Format number as K (e.g., 84000 -> "084K")
|
|
100
|
+
*/
|
|
101
|
+
function formatTokens(tokens) {
|
|
102
|
+
const k = Math.floor(tokens / 1000);
|
|
103
|
+
return k.toString().padStart(3, '0') + 'K';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate smooth bar using Braille characters (8 levels per character)
|
|
108
|
+
*/
|
|
109
|
+
function generateBar(percentage, width = 20) {
|
|
110
|
+
const brailleSteps = ['⠀', '⡀', '⣀', '⣄', '⣤', '⣦', '⣶', '⣷', '⣿'];
|
|
111
|
+
|
|
112
|
+
const totalSteps = width * 8; // 8 sub-steps per character
|
|
113
|
+
const filledSteps = Math.round((percentage / 100) * totalSteps);
|
|
114
|
+
|
|
115
|
+
let bar = '';
|
|
116
|
+
for (let i = 0; i < width; i++) {
|
|
117
|
+
const charSteps = filledSteps - i * 8;
|
|
118
|
+
if (charSteps >= 8) {
|
|
119
|
+
bar += '⣿'; // Fully filled
|
|
120
|
+
} else if (charSteps > 0) {
|
|
121
|
+
bar += brailleSteps[charSteps]; // Partially filled
|
|
122
|
+
} else {
|
|
123
|
+
bar += '⠀'; // Empty
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return bar;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get greyscale intensity based on token percentage
|
|
132
|
+
*/
|
|
133
|
+
function getContextGreyscale(percentage) {
|
|
134
|
+
// Use ANSI 256 colors for greyscale (232-255 are grey shades)
|
|
135
|
+
// Darker grey for low usage, lighter/white for high usage
|
|
136
|
+
if (percentage >= 90) return '\x1b[38;5;255m'; // Bright white - critical
|
|
137
|
+
if (percentage >= 75) return '\x1b[38;5;250m'; // Light grey - warning
|
|
138
|
+
if (percentage >= 50) return '\x1b[38;5;245m'; // Medium grey
|
|
139
|
+
return '\x1b[38;5;240m'; // Dark grey - safe
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Count files in a directory recursively
|
|
144
|
+
*/
|
|
145
|
+
function countFilesInDirectory(dirPath) {
|
|
146
|
+
try {
|
|
147
|
+
let count = 0;
|
|
148
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
149
|
+
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
const fullPath = join(dirPath, entry.name);
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
count += countFilesInDirectory(fullPath);
|
|
154
|
+
} else {
|
|
155
|
+
count++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return count;
|
|
160
|
+
} catch {
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get git status counts
|
|
167
|
+
*/
|
|
168
|
+
function getGitStatus(projectDir) {
|
|
169
|
+
try {
|
|
170
|
+
const output = execSync('git status --short', {
|
|
171
|
+
cwd: projectDir,
|
|
172
|
+
encoding: 'utf-8',
|
|
173
|
+
stdio: ['pipe', 'pipe', 'ignore'], // Suppress stderr
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const counts = { modified: 0, added: 0, deleted: 0, untracked: 0 };
|
|
177
|
+
|
|
178
|
+
for (const line of output.split('\n')) {
|
|
179
|
+
if (!line.trim()) continue;
|
|
180
|
+
|
|
181
|
+
const status = line.substring(0, 2);
|
|
182
|
+
if (status.includes('M')) counts.modified++;
|
|
183
|
+
if (status.includes('A')) counts.added++;
|
|
184
|
+
if (status.includes('D')) counts.deleted++;
|
|
185
|
+
|
|
186
|
+
// Handle untracked files/directories
|
|
187
|
+
if (status.includes('?')) {
|
|
188
|
+
const filePath = line.substring(3).trim(); // Remove '?? ' prefix
|
|
189
|
+
const fullPath = join(projectDir, filePath);
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const stats = statSync(fullPath);
|
|
193
|
+
if (stats.isDirectory()) {
|
|
194
|
+
// Count all files in the directory
|
|
195
|
+
counts.untracked += countFilesInDirectory(fullPath);
|
|
196
|
+
} else {
|
|
197
|
+
// Single file
|
|
198
|
+
counts.untracked++;
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
// If stat fails, count as 1
|
|
202
|
+
counts.untracked++;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return counts;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Format git status counts as string
|
|
215
|
+
*/
|
|
216
|
+
function formatGitStatus(counts) {
|
|
217
|
+
const parts = [];
|
|
218
|
+
|
|
219
|
+
if (counts.modified > 0) parts.push(`${counts.modified}M`);
|
|
220
|
+
if (counts.added > 0) parts.push(`${counts.added}A`);
|
|
221
|
+
if (counts.deleted > 0) parts.push(`${counts.deleted}D`);
|
|
222
|
+
if (counts.untracked > 0) parts.push(`${counts.untracked}U`);
|
|
223
|
+
|
|
224
|
+
return parts.length > 0 ? `± ${parts.join('/')}` : '';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get current git branch name
|
|
229
|
+
*/
|
|
230
|
+
function getGitBranch(projectDir) {
|
|
231
|
+
try {
|
|
232
|
+
const branch = execSync('git branch --show-current', {
|
|
233
|
+
cwd: projectDir,
|
|
234
|
+
encoding: 'utf-8',
|
|
235
|
+
stdio: ['pipe', 'pipe', 'ignore'], // Suppress stderr
|
|
236
|
+
}).trim();
|
|
237
|
+
return branch || null;
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Main function
|
|
245
|
+
*/
|
|
246
|
+
function main() {
|
|
247
|
+
try {
|
|
248
|
+
// Read input from stdin
|
|
249
|
+
let input = readFileSync(0, 'utf-8');
|
|
250
|
+
|
|
251
|
+
// Try to parse JSON - if it fails due to Windows path escaping, attempt to fix it
|
|
252
|
+
let data;
|
|
253
|
+
try {
|
|
254
|
+
data = JSON.parse(input);
|
|
255
|
+
} catch {
|
|
256
|
+
// Windows path escaping issue - replace backslashes with forward slashes
|
|
257
|
+
input = input.replace(/\\\\/g, '/');
|
|
258
|
+
data = JSON.parse(input);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Normalize all Windows paths to forward slashes (Node.js handles both on Windows)
|
|
262
|
+
if (data.workspace) {
|
|
263
|
+
data.workspace.project_dir = data.workspace.project_dir.replace(/\\/g, '/');
|
|
264
|
+
data.workspace.current_dir = data.workspace.current_dir.replace(/\\/g, '/');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ANSI color codes
|
|
268
|
+
const DIM_GREY = '\x1b[38;5;245m';
|
|
269
|
+
const RESET = '\x1b[0m';
|
|
270
|
+
|
|
271
|
+
// Get token usage from transcript (use transcript_path directly from input)
|
|
272
|
+
let tokenData = null;
|
|
273
|
+
if (data.transcript_path) {
|
|
274
|
+
tokenData = calculateTokenUsage(data.transcript_path);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Generate context display
|
|
278
|
+
let ctxDisplay = 'CTX: [NO DATA]';
|
|
279
|
+
if (tokenData) {
|
|
280
|
+
const bar = generateBar(tokenData.percentage, 20);
|
|
281
|
+
const usedTokens = formatTokens(tokenData.used);
|
|
282
|
+
const limitTokens = formatTokens(tokenData.limit);
|
|
283
|
+
const pct = tokenData.percentage.toString().padStart(2, ' ');
|
|
284
|
+
|
|
285
|
+
// Get greyscale intensity for this percentage
|
|
286
|
+
const greyColor = getContextGreyscale(tokenData.percentage);
|
|
287
|
+
|
|
288
|
+
// Apply greyscale to just the bar and percentage, then return to dim grey for rest
|
|
289
|
+
ctxDisplay = `CTX: ${greyColor}[${bar}] ${pct}%${DIM_GREY} | ${usedTokens}/${limitTokens}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Get git information
|
|
293
|
+
const gitBranch = getGitBranch(data.workspace.project_dir);
|
|
294
|
+
const gitStatus = getGitStatus(data.workspace.project_dir);
|
|
295
|
+
const gitStatusStr = gitStatus ? formatGitStatus(gitStatus) : '';
|
|
296
|
+
|
|
297
|
+
// Multi-line bordered box format
|
|
298
|
+
const width = 56;
|
|
299
|
+
|
|
300
|
+
// Top border with model name (add 2 to match content line width: ║ + space + 56 + space + ║ = 60)
|
|
301
|
+
const modelName = data.model.display_name.toUpperCase();
|
|
302
|
+
const labelWidth = modelName.length + 2; // +2 for brackets
|
|
303
|
+
const topBorder = `${DIM_GREY}╔═[${modelName}]${'═'.repeat(width - labelWidth + 1)}╗${RESET}`;
|
|
304
|
+
|
|
305
|
+
// Line 0: Directory + Git branch + status (padded to width)
|
|
306
|
+
const rawDirName = basename(data.workspace.project_dir).toUpperCase();
|
|
307
|
+
let line0Content = '';
|
|
308
|
+
|
|
309
|
+
if (gitBranch) {
|
|
310
|
+
// Calculate available space: total width - "DIR: " - " | " - git status
|
|
311
|
+
const dirPrefix = 'DIR: ';
|
|
312
|
+
const separator = ' | ';
|
|
313
|
+
const gitStatusSpace = gitStatusStr ? ` ${gitStatusStr}` : '';
|
|
314
|
+
|
|
315
|
+
// Reserve space for branch indicator and git status
|
|
316
|
+
const branchSuffix = ' ⎇';
|
|
317
|
+
const reservedSpace =
|
|
318
|
+
dirPrefix.length + separator.length + branchSuffix.length + gitStatusSpace.length;
|
|
319
|
+
const availableSpace = width - reservedSpace;
|
|
320
|
+
|
|
321
|
+
// Split available space between directory and branch (favor branch slightly)
|
|
322
|
+
const maxDirLen = Math.floor(availableSpace * 0.4);
|
|
323
|
+
const maxBranchLen = availableSpace - maxDirLen;
|
|
324
|
+
|
|
325
|
+
const dirName =
|
|
326
|
+
rawDirName.length > maxDirLen ? rawDirName.substring(0, maxDirLen - 3) + '...' : rawDirName;
|
|
327
|
+
|
|
328
|
+
const displayBranch =
|
|
329
|
+
gitBranch.length > maxBranchLen
|
|
330
|
+
? gitBranch.substring(0, maxBranchLen - 3) + '...'
|
|
331
|
+
: gitBranch;
|
|
332
|
+
|
|
333
|
+
line0Content = `${dirPrefix}${dirName}${separator}${displayBranch.toUpperCase()}${branchSuffix}${gitStatusSpace}`;
|
|
334
|
+
} else {
|
|
335
|
+
// No git info, just show directory
|
|
336
|
+
const maxDirLen = 20;
|
|
337
|
+
const dirName =
|
|
338
|
+
rawDirName.length > maxDirLen ? rawDirName.substring(0, maxDirLen - 3) + '...' : rawDirName;
|
|
339
|
+
line0Content = `DIR: ${dirName}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const plainLine0 = line0Content.replace(/\x1b\[[0-9;]*m/g, '');
|
|
343
|
+
const padding0 = width - plainLine0.length;
|
|
344
|
+
const paddedLine0 = line0Content + ' '.repeat(Math.max(0, padding0));
|
|
345
|
+
const line0 = `${DIM_GREY}║ ${paddedLine0} ║${RESET}`;
|
|
346
|
+
|
|
347
|
+
// Line 1: Context bar (padded to width)
|
|
348
|
+
const plainCtx = ctxDisplay.replace(/\x1b\[[0-9;]*m/g, '');
|
|
349
|
+
const padding1 = width - plainCtx.length;
|
|
350
|
+
const paddedCtx = ctxDisplay + ' '.repeat(Math.max(0, padding1));
|
|
351
|
+
const line1 = `${DIM_GREY}║ ${paddedCtx} ║${RESET}`;
|
|
352
|
+
|
|
353
|
+
// Bottom border (add 2 to match content line width)
|
|
354
|
+
const bottomBorder = `${DIM_GREY}╚${'═'.repeat(width + 2)}╝${RESET}`;
|
|
355
|
+
|
|
356
|
+
console.log(`${topBorder}\n${line0}\n${line1}\n${bottomBorder}`);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
// Log error for debugging (goes to stderr, not visible in status line)
|
|
359
|
+
console.error(`Status line error: ${error.message}`);
|
|
360
|
+
// Fallback to simple status on any error
|
|
361
|
+
console.log('—');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
main();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* validate-yaml.js - Validate YAML syntax in files
|
|
5
|
+
*
|
|
6
|
+
* This script validates YAML files after they are created or modified through Claude Code's
|
|
7
|
+
* Edit or Write tools. It's invoked by the PostToolUse hook system with JSON input via stdin
|
|
8
|
+
* containing the file path that was just modified.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* echo '{"tool_input":{"file_path":"/path/to/file.yaml"}}' | node validate-yaml.js
|
|
12
|
+
*
|
|
13
|
+
* Input format (JSON via stdin):
|
|
14
|
+
* {
|
|
15
|
+
* "tool_input": {
|
|
16
|
+
* "file_path": "/absolute/path/to/file.yaml"
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Exit codes:
|
|
21
|
+
* 0 - File is valid YAML or not a YAML file (silent success)
|
|
22
|
+
* 2 - File is invalid YAML (parsing error written to stderr)
|
|
23
|
+
*
|
|
24
|
+
* Behavior:
|
|
25
|
+
* - Non-YAML files (.yaml and .yml extensions) pass through silently with exit 0
|
|
26
|
+
* - Valid YAML files produce no output and exit 0
|
|
27
|
+
* - Invalid YAML files produce parsing error on stderr and exit 2
|
|
28
|
+
*
|
|
29
|
+
* Dependencies:
|
|
30
|
+
* - js-yaml (for YAML parsing)
|
|
31
|
+
* - Node.js built-ins (fs, path)
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const yaml = require('js-yaml');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read JSON from stdin and extract file_path
|
|
40
|
+
* @returns {Promise<string | null>} - The file path from tool_input, or null if invalid
|
|
41
|
+
*/
|
|
42
|
+
async function readStdin() {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
let data = '';
|
|
45
|
+
|
|
46
|
+
process.stdin.setEncoding('utf8');
|
|
47
|
+
process.stdin.on('data', chunk => {
|
|
48
|
+
data += chunk;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
process.stdin.on('end', () => {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(data);
|
|
54
|
+
const filePath = parsed?.tool_input?.file_path;
|
|
55
|
+
resolve(filePath || null);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// If we can't parse the input, still exit cleanly - don't interrupt the user's work
|
|
58
|
+
resolve(null);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
process.stdin.on('error', () => {
|
|
63
|
+
// If there's an issue reading stdin, exit cleanly
|
|
64
|
+
resolve(null);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if a file has a YAML extension
|
|
71
|
+
* @param {string} filePath - The file path to check
|
|
72
|
+
* @returns {boolean} - True if the file ends with .yaml or .yml
|
|
73
|
+
*/
|
|
74
|
+
function isYamlFile(filePath) {
|
|
75
|
+
return filePath.endsWith('.yaml') || filePath.endsWith('.yml');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate YAML syntax by attempting to parse the file
|
|
80
|
+
* @param {string} filePath - The absolute path to the YAML file
|
|
81
|
+
* @returns {{ valid: boolean, error?: string }} - Validation result with optional error message
|
|
82
|
+
*/
|
|
83
|
+
function validateYaml(filePath) {
|
|
84
|
+
try {
|
|
85
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
86
|
+
yaml.load(content);
|
|
87
|
+
return { valid: true };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
error: error.message
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Main execution - validate YAML file from tool input
|
|
98
|
+
*/
|
|
99
|
+
async function main() {
|
|
100
|
+
const filePath = await readStdin();
|
|
101
|
+
|
|
102
|
+
// If we couldn't extract a file path, exit silently
|
|
103
|
+
if (!filePath) {
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// If it's not a YAML file, pass through silently
|
|
108
|
+
if (!isYamlFile(filePath)) {
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate the YAML file
|
|
113
|
+
const result = validateYaml(filePath);
|
|
114
|
+
|
|
115
|
+
if (result.valid) {
|
|
116
|
+
// Valid YAML - silent success
|
|
117
|
+
process.exit(0);
|
|
118
|
+
} else {
|
|
119
|
+
// Invalid YAML - report error to stderr
|
|
120
|
+
console.error(`YAML validation error in ${filePath}:\n${result.error}`);
|
|
121
|
+
process.exit(2);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
main().catch(() => {
|
|
126
|
+
// If anything goes wrong, exit silently to avoid disrupting the user's workflow
|
|
127
|
+
process.exit(0);
|
|
128
|
+
});
|