aiblueprint-cli 1.4.12 → 1.4.13
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-code-config/scripts/.claude/commands/fix-on-my-computer.md +87 -0
- package/claude-code-config/scripts/command-validator/CLAUDE.md +112 -0
- package/claude-code-config/scripts/command-validator/src/__tests__/validator.test.ts +62 -111
- package/claude-code-config/scripts/command-validator/src/cli.ts +5 -3
- package/claude-code-config/scripts/command-validator/src/lib/security-rules.ts +3 -4
- package/claude-code-config/scripts/command-validator/src/lib/types.ts +1 -0
- package/claude-code-config/scripts/command-validator/src/lib/validator.ts +47 -317
- package/claude-code-config/scripts/statusline/CLAUDE.md +29 -7
- package/claude-code-config/scripts/statusline/README.md +89 -1
- package/claude-code-config/scripts/statusline/defaults.json +75 -0
- package/claude-code-config/scripts/statusline/src/index.ts +101 -24
- package/claude-code-config/scripts/statusline/src/lib/config-types.ts +100 -0
- package/claude-code-config/scripts/statusline/src/lib/config.ts +21 -0
- package/claude-code-config/scripts/statusline/src/lib/context.ts +32 -11
- package/claude-code-config/scripts/statusline/src/lib/formatters.ts +360 -22
- package/claude-code-config/scripts/statusline/src/lib/git.ts +100 -0
- package/claude-code-config/scripts/statusline/src/lib/render-pure.ts +177 -0
- package/claude-code-config/scripts/statusline/src/lib/types.ts +11 -0
- package/claude-code-config/scripts/statusline/statusline.config.json +93 -0
- package/claude-code-config/skills/claude-memory/SKILL.md +689 -0
- package/claude-code-config/skills/claude-memory/references/comprehensive-example.md +175 -0
- package/claude-code-config/skills/claude-memory/references/project-patterns.md +334 -0
- package/claude-code-config/skills/claude-memory/references/prompting-techniques.md +411 -0
- package/claude-code-config/skills/claude-memory/references/section-templates.md +347 -0
- package/claude-code-config/skills/create-slash-commands/SKILL.md +1110 -0
- package/claude-code-config/skills/create-slash-commands/references/arguments.md +273 -0
- package/claude-code-config/skills/create-slash-commands/references/patterns.md +947 -0
- package/claude-code-config/skills/create-slash-commands/references/prompt-examples.md +656 -0
- package/claude-code-config/skills/create-slash-commands/references/tool-restrictions.md +389 -0
- package/claude-code-config/skills/create-subagents/SKILL.md +425 -0
- package/claude-code-config/skills/create-subagents/references/context-management.md +567 -0
- package/claude-code-config/skills/create-subagents/references/debugging-agents.md +714 -0
- package/claude-code-config/skills/create-subagents/references/error-handling-and-recovery.md +502 -0
- package/claude-code-config/skills/create-subagents/references/evaluation-and-testing.md +374 -0
- package/claude-code-config/skills/create-subagents/references/orchestration-patterns.md +591 -0
- package/claude-code-config/skills/create-subagents/references/subagents.md +599 -0
- package/claude-code-config/skills/create-subagents/references/writing-subagent-prompts.md +513 -0
- package/package.json +1 -1
- package/claude-code-config/commands/apex.md +0 -109
- package/claude-code-config/commands/tasks/run-task.md +0 -220
- package/claude-code-config/commands/utils/watch-ci.md +0 -47
- package/claude-code-config/scripts/command-validator/biome.json +0 -29
- package/claude-code-config/scripts/command-validator/bun.lockb +0 -0
- package/claude-code-config/scripts/command-validator/package.json +0 -27
- package/claude-code-config/scripts/command-validator/vitest.config.ts +0 -7
- package/claude-code-config/scripts/hook-post-file.ts +0 -162
- package/claude-code-config/scripts/statusline/biome.json +0 -34
- package/claude-code-config/scripts/statusline/bun.lockb +0 -0
- package/claude-code-config/scripts/statusline/fixtures/test-input.json +0 -25
- package/claude-code-config/scripts/statusline/package.json +0 -19
- package/claude-code-config/scripts/statusline/statusline.config.ts +0 -25
- package/claude-code-config/scripts/statusline/test.ts +0 -20
- package/claude-code-config/scripts/validate-command.js +0 -712
- package/claude-code-config/scripts/validate-command.readme.md +0 -283
|
@@ -1,360 +1,90 @@
|
|
|
1
|
-
import { SAFE_COMMANDS, SECURITY_RULES } from "./security-rules";
|
|
2
1
|
import type { ValidationResult } from "./types";
|
|
3
2
|
|
|
3
|
+
const DANGEROUS_COMMANDS = [
|
|
4
|
+
"sudo",
|
|
5
|
+
"su",
|
|
6
|
+
"chmod",
|
|
7
|
+
"chown",
|
|
8
|
+
"dd",
|
|
9
|
+
"mkfs",
|
|
10
|
+
"fdisk",
|
|
11
|
+
"kill",
|
|
12
|
+
"killall",
|
|
13
|
+
];
|
|
14
|
+
|
|
4
15
|
export class CommandValidator {
|
|
5
|
-
validate(command: string,
|
|
16
|
+
validate(command: string, _toolName = "Unknown"): ValidationResult {
|
|
6
17
|
const result: ValidationResult = {
|
|
7
18
|
isValid: true,
|
|
8
19
|
severity: "LOW",
|
|
9
20
|
violations: [],
|
|
10
21
|
sanitizedCommand: command,
|
|
22
|
+
action: "allow",
|
|
11
23
|
};
|
|
12
24
|
|
|
13
25
|
if (!command || typeof command !== "string") {
|
|
14
26
|
result.isValid = false;
|
|
15
27
|
result.violations.push("Invalid command format");
|
|
28
|
+
result.action = "deny";
|
|
16
29
|
return result;
|
|
17
30
|
}
|
|
18
31
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
result.severity = "MEDIUM";
|
|
22
|
-
result.violations.push("Command too long (potential buffer overflow)");
|
|
23
|
-
return result;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\xFF]/.test(command)) {
|
|
27
|
-
result.isValid = false;
|
|
28
|
-
result.severity = "HIGH";
|
|
29
|
-
result.violations.push("Binary or encoded content detected");
|
|
30
|
-
return result;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const normalizedCmd = command.trim().toLowerCase();
|
|
34
|
-
const cmdParts = normalizedCmd.split(/\s+/);
|
|
35
|
-
const mainCommand = cmdParts[0].split("/").pop() || "";
|
|
36
|
-
|
|
37
|
-
if (mainCommand === "source" || mainCommand === "python") {
|
|
38
|
-
return result;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
for (const pattern of SECURITY_RULES.DANGEROUS_PATTERNS) {
|
|
42
|
-
if (pattern.test(command)) {
|
|
43
|
-
result.isValid = false;
|
|
44
|
-
result.severity = "CRITICAL";
|
|
45
|
-
result.violations.push(`Dangerous pattern detected: ${pattern.source}`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (SECURITY_RULES.CRITICAL_COMMANDS.includes(mainCommand)) {
|
|
32
|
+
// rm -rf → DENY (blocked completely)
|
|
33
|
+
if (this.containsRmRf(command)) {
|
|
50
34
|
result.isValid = false;
|
|
51
35
|
result.severity = "CRITICAL";
|
|
52
|
-
result.violations.push(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (SECURITY_RULES.PRIVILEGE_COMMANDS.includes(mainCommand)) {
|
|
56
|
-
result.isValid = false;
|
|
57
|
-
result.severity = "HIGH";
|
|
58
|
-
result.violations.push(`Privilege escalation command: ${mainCommand}`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (SECURITY_RULES.NETWORK_COMMANDS.includes(mainCommand)) {
|
|
62
|
-
result.isValid = false;
|
|
63
|
-
result.severity = "HIGH";
|
|
64
|
-
result.violations.push(`Network/remote access command: ${mainCommand}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (SECURITY_RULES.SYSTEM_COMMANDS.includes(mainCommand)) {
|
|
68
|
-
result.isValid = false;
|
|
69
|
-
result.severity = "HIGH";
|
|
70
|
-
result.violations.push(`System manipulation command: ${mainCommand}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (/rm\s+.*-rf\s/.test(command)) {
|
|
74
|
-
const isRmRfSafe = this.isRmRfCommandSafe(command);
|
|
75
|
-
if (!isRmRfSafe) {
|
|
76
|
-
result.isValid = false;
|
|
77
|
-
result.severity = "CRITICAL";
|
|
78
|
-
result.violations.push("rm -rf command targeting unsafe path");
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (SAFE_COMMANDS.includes(mainCommand) && result.violations.length === 0) {
|
|
83
|
-
return result;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (command.includes("&&")) {
|
|
87
|
-
const chainedCommands = this.splitCommandChain(command);
|
|
88
|
-
let allSafe = true;
|
|
89
|
-
for (const chainedCmd of chainedCommands) {
|
|
90
|
-
const trimmedCmd = chainedCmd.trim();
|
|
91
|
-
const cmdParts = trimmedCmd.split(/\s+/);
|
|
92
|
-
const mainCommand = cmdParts[0];
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
mainCommand === "source" ||
|
|
96
|
-
mainCommand === "python" ||
|
|
97
|
-
SAFE_COMMANDS.includes(mainCommand)
|
|
98
|
-
) {
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const chainResult = this.validateSingleCommand(trimmedCmd, toolName);
|
|
103
|
-
if (!chainResult.isValid) {
|
|
104
|
-
result.isValid = false;
|
|
105
|
-
result.severity = chainResult.severity;
|
|
106
|
-
result.violations.push(
|
|
107
|
-
`Chained command violation: ${trimmedCmd} - ${chainResult.violations.join(", ")}`,
|
|
108
|
-
);
|
|
109
|
-
allSafe = false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (allSafe) {
|
|
113
|
-
return result;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (command.includes(";") || command.includes("||")) {
|
|
118
|
-
const chainedCommands = this.splitCommandChain(command);
|
|
119
|
-
for (const chainedCmd of chainedCommands) {
|
|
120
|
-
const chainResult = this.validateSingleCommand(
|
|
121
|
-
chainedCmd.trim(),
|
|
122
|
-
toolName,
|
|
123
|
-
);
|
|
124
|
-
if (!chainResult.isValid) {
|
|
125
|
-
result.isValid = false;
|
|
126
|
-
result.severity = chainResult.severity;
|
|
127
|
-
result.violations.push(
|
|
128
|
-
`Chained command violation: ${chainedCmd.trim()} - ${chainResult.violations.join(", ")}`,
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
36
|
+
result.violations.push("rm -rf is forbidden - use trash instead");
|
|
37
|
+
result.action = "deny";
|
|
132
38
|
return result;
|
|
133
39
|
}
|
|
134
40
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
path === "/dev/" &&
|
|
139
|
-
(command.includes("/dev/null") ||
|
|
140
|
-
command.includes("/dev/stderr") ||
|
|
141
|
-
command.includes("/dev/stdout"))
|
|
142
|
-
) {
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const cmdStart = command.trim();
|
|
147
|
-
let isSafeExecutable = false;
|
|
148
|
-
for (const safePath of SECURITY_RULES.SAFE_EXECUTABLE_PATHS) {
|
|
149
|
-
if (cmdStart.startsWith(safePath)) {
|
|
150
|
-
isSafeExecutable = true;
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const pathIndex = command.indexOf(path);
|
|
156
|
-
const beforePath = command.substring(0, pathIndex);
|
|
157
|
-
const redirectBeforePath = />\s*$/.test(beforePath.trim());
|
|
158
|
-
|
|
159
|
-
if (!isSafeExecutable && redirectBeforePath) {
|
|
160
|
-
result.isValid = false;
|
|
161
|
-
result.severity = "HIGH";
|
|
162
|
-
result.violations.push(
|
|
163
|
-
`Dangerous operation on protected path: ${path}`,
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return result;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
validateSingleCommand(
|
|
173
|
-
command: string,
|
|
174
|
-
_toolName = "Unknown",
|
|
175
|
-
): ValidationResult {
|
|
176
|
-
const result: ValidationResult = {
|
|
177
|
-
isValid: true,
|
|
178
|
-
severity: "LOW",
|
|
179
|
-
violations: [],
|
|
180
|
-
sanitizedCommand: command,
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
if (!command || typeof command !== "string") {
|
|
184
|
-
result.isValid = false;
|
|
185
|
-
result.violations.push("Invalid command format");
|
|
186
|
-
return result;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (command.length > 2000) {
|
|
190
|
-
result.isValid = false;
|
|
191
|
-
result.severity = "MEDIUM";
|
|
192
|
-
result.violations.push("Command too long (potential buffer overflow)");
|
|
193
|
-
return result;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\xFF]/.test(command)) {
|
|
41
|
+
// Other dangerous commands → ASK (ask for permission)
|
|
42
|
+
const dangerousCmd = this.containsDangerousCommand(command);
|
|
43
|
+
if (dangerousCmd) {
|
|
197
44
|
result.isValid = false;
|
|
198
45
|
result.severity = "HIGH";
|
|
199
|
-
result.violations.push(
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const normalizedCmd = command.trim().toLowerCase();
|
|
204
|
-
const cmdParts = normalizedCmd.split(/\s+/);
|
|
205
|
-
const mainCommand = cmdParts[0].split("/").pop() || "";
|
|
206
|
-
|
|
207
|
-
if (mainCommand === "source" || mainCommand === "python") {
|
|
46
|
+
result.violations.push(`Potentially dangerous command: ${dangerousCmd}`);
|
|
47
|
+
result.action = "ask";
|
|
208
48
|
return result;
|
|
209
49
|
}
|
|
210
50
|
|
|
211
|
-
for (const pattern of SECURITY_RULES.DANGEROUS_PATTERNS) {
|
|
212
|
-
if (pattern.test(command)) {
|
|
213
|
-
result.isValid = false;
|
|
214
|
-
result.severity = "CRITICAL";
|
|
215
|
-
result.violations.push(`Dangerous pattern detected: ${pattern.source}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (SECURITY_RULES.CRITICAL_COMMANDS.includes(mainCommand)) {
|
|
220
|
-
result.isValid = false;
|
|
221
|
-
result.severity = "CRITICAL";
|
|
222
|
-
result.violations.push(`Critical dangerous command: ${mainCommand}`);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (SECURITY_RULES.PRIVILEGE_COMMANDS.includes(mainCommand)) {
|
|
226
|
-
result.isValid = false;
|
|
227
|
-
result.severity = "HIGH";
|
|
228
|
-
result.violations.push(`Privilege escalation command: ${mainCommand}`);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (SECURITY_RULES.NETWORK_COMMANDS.includes(mainCommand)) {
|
|
232
|
-
result.isValid = false;
|
|
233
|
-
result.severity = "HIGH";
|
|
234
|
-
result.violations.push(`Network/remote access command: ${mainCommand}`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (SECURITY_RULES.SYSTEM_COMMANDS.includes(mainCommand)) {
|
|
238
|
-
result.isValid = false;
|
|
239
|
-
result.severity = "HIGH";
|
|
240
|
-
result.violations.push(`System manipulation command: ${mainCommand}`);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (/rm\s+.*-rf\s/.test(command)) {
|
|
244
|
-
const isRmRfSafe = this.isRmRfCommandSafe(command);
|
|
245
|
-
if (!isRmRfSafe) {
|
|
246
|
-
result.isValid = false;
|
|
247
|
-
result.severity = "CRITICAL";
|
|
248
|
-
result.violations.push("rm -rf command targeting unsafe path");
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (SAFE_COMMANDS.includes(mainCommand) && result.violations.length === 0) {
|
|
253
|
-
return result;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
for (const path of SECURITY_RULES.PROTECTED_PATHS) {
|
|
257
|
-
if (command.includes(path)) {
|
|
258
|
-
if (
|
|
259
|
-
path === "/dev/" &&
|
|
260
|
-
(command.includes("/dev/null") ||
|
|
261
|
-
command.includes("/dev/stderr") ||
|
|
262
|
-
command.includes("/dev/stdout"))
|
|
263
|
-
) {
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const cmdStart = command.trim();
|
|
268
|
-
let isSafeExecutable = false;
|
|
269
|
-
for (const safePath of SECURITY_RULES.SAFE_EXECUTABLE_PATHS) {
|
|
270
|
-
if (cmdStart.startsWith(safePath)) {
|
|
271
|
-
isSafeExecutable = true;
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const pathIndex = command.indexOf(path);
|
|
277
|
-
const beforePath = command.substring(0, pathIndex);
|
|
278
|
-
const redirectBeforePath = />\s*$/.test(beforePath.trim());
|
|
279
|
-
|
|
280
|
-
if (!isSafeExecutable && redirectBeforePath) {
|
|
281
|
-
result.isValid = false;
|
|
282
|
-
result.severity = "HIGH";
|
|
283
|
-
result.violations.push(
|
|
284
|
-
`Dangerous operation on protected path: ${path}`,
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
51
|
return result;
|
|
291
52
|
}
|
|
292
53
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const char = command[i];
|
|
301
|
-
const nextChar = command[i + 1];
|
|
54
|
+
containsRmRf(command: string): boolean {
|
|
55
|
+
// Check for rm -rf in any form (rm -rf, rm -fr, rm -r -f, etc.)
|
|
56
|
+
const rmRfPatterns = [
|
|
57
|
+
/\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*r[a-zA-Z]*)\s/i,
|
|
58
|
+
/\brm\s+-r\s+-f\s/i,
|
|
59
|
+
/\brm\s+-f\s+-r\s/i,
|
|
60
|
+
];
|
|
302
61
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
current += char;
|
|
307
|
-
} else if (char === quoteChar && inQuotes) {
|
|
308
|
-
inQuotes = false;
|
|
309
|
-
quoteChar = "";
|
|
310
|
-
current += char;
|
|
311
|
-
} else if (inQuotes) {
|
|
312
|
-
current += char;
|
|
313
|
-
} else if (char === "&" && nextChar === "&") {
|
|
314
|
-
commands.push(current.trim());
|
|
315
|
-
current = "";
|
|
316
|
-
i++;
|
|
317
|
-
} else if (char === "|" && nextChar === "|") {
|
|
318
|
-
commands.push(current.trim());
|
|
319
|
-
current = "";
|
|
320
|
-
i++;
|
|
321
|
-
} else if (char === ";") {
|
|
322
|
-
commands.push(current.trim());
|
|
323
|
-
current = "";
|
|
324
|
-
} else {
|
|
325
|
-
current += char;
|
|
62
|
+
for (const pattern of rmRfPatterns) {
|
|
63
|
+
if (pattern.test(command)) {
|
|
64
|
+
return true;
|
|
326
65
|
}
|
|
327
66
|
}
|
|
328
67
|
|
|
329
|
-
|
|
330
|
-
commands.push(current.trim());
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return commands.filter((cmd) => cmd.length > 0);
|
|
68
|
+
return false;
|
|
334
69
|
}
|
|
335
70
|
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const targetPath = rmRfMatch[1];
|
|
71
|
+
containsDangerousCommand(command: string): string | null {
|
|
72
|
+
const normalizedCmd = command.trim().toLowerCase();
|
|
73
|
+
const parts = normalizedCmd.split(/\s+/);
|
|
74
|
+
const mainCommand = parts[0].split("/").pop() || "";
|
|
343
75
|
|
|
344
|
-
if (
|
|
345
|
-
return
|
|
76
|
+
if (DANGEROUS_COMMANDS.includes(mainCommand)) {
|
|
77
|
+
return mainCommand;
|
|
346
78
|
}
|
|
347
79
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
80
|
+
// Check in chained commands
|
|
81
|
+
for (const dangerous of DANGEROUS_COMMANDS) {
|
|
82
|
+
const pattern = new RegExp(`\\b${dangerous}\\b`, "i");
|
|
83
|
+
if (pattern.test(command)) {
|
|
84
|
+
return dangerous;
|
|
351
85
|
}
|
|
352
86
|
}
|
|
353
87
|
|
|
354
|
-
|
|
355
|
-
return true;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return false;
|
|
88
|
+
return null;
|
|
359
89
|
}
|
|
360
90
|
}
|
|
@@ -7,6 +7,7 @@ Clean, type-safe statusline implementation for Claude Code using Bun + TypeScrip
|
|
|
7
7
|
## Project Setup & Configuration
|
|
8
8
|
|
|
9
9
|
### Dependencies
|
|
10
|
+
|
|
10
11
|
- **Bun**: Runtime (uses `$` for shell commands)
|
|
11
12
|
- **@biomejs/biome**: Linting & formatting
|
|
12
13
|
- **TypeScript**: Type safety
|
|
@@ -29,16 +30,12 @@ Add to `~/.claude/settings.json`:
|
|
|
29
30
|
|
|
30
31
|
### Authentication
|
|
31
32
|
|
|
32
|
-
OAuth token
|
|
33
|
+
OAuth token stored in macOS Keychain:
|
|
33
34
|
|
|
34
|
-
**macOS**: Stored in Keychain
|
|
35
35
|
- **Service**: `Claude Code-credentials`
|
|
36
|
-
- **Access**: `security find-generic-password -s "Claude Code-credentials" -w`
|
|
37
|
-
|
|
38
|
-
**Windows & Linux**: Stored in file system
|
|
39
|
-
- **Location**: `~/.claude/.credentials.json`
|
|
40
36
|
- **Format**: JSON with `claudeAiOauth.accessToken`
|
|
41
37
|
- **Token type**: `sk-ant-oat01-...` (OAuth token, not API key)
|
|
38
|
+
- **Access**: `security find-generic-password -s "Claude Code-credentials" -w`
|
|
42
39
|
|
|
43
40
|
## Architecture
|
|
44
41
|
|
|
@@ -78,6 +75,7 @@ Claude Code Hook → stdin JSON → index.ts
|
|
|
78
75
|
## Component Specifications
|
|
79
76
|
|
|
80
77
|
### Context Calculation (`lib/context.ts`)
|
|
78
|
+
|
|
81
79
|
- **Purpose**: Calculate token usage from Claude Code transcript files
|
|
82
80
|
- **Algorithm**: Parses `.jsonl` transcript, finds most recent main-chain entry
|
|
83
81
|
- **Tokens counted**: `input_tokens + cache_read_input_tokens + cache_creation_input_tokens`
|
|
@@ -85,6 +83,7 @@ Claude Code Hook → stdin JSON → index.ts
|
|
|
85
83
|
- **Output**: `{ tokens: number, percentage: number }` (0-100% of 200k context)
|
|
86
84
|
|
|
87
85
|
### Usage Limits (`lib/usage-limits.ts`)
|
|
86
|
+
|
|
88
87
|
- **Purpose**: Fetch Claude API rate limits from OAuth endpoint
|
|
89
88
|
- **Auth**: Retrieves OAuth token from macOS Keychain (`Claude Code-credentials`)
|
|
90
89
|
- **API**: `https://api.anthropic.com/api/oauth/usage`
|
|
@@ -92,12 +91,14 @@ Claude Code Hook → stdin JSON → index.ts
|
|
|
92
91
|
- **Error handling**: Fails silently, returns null on errors
|
|
93
92
|
|
|
94
93
|
### Git Status (`lib/git.ts`)
|
|
94
|
+
|
|
95
95
|
- **Purpose**: Show current branch and uncommitted changes
|
|
96
96
|
- **Detection**: Checks both staged and unstaged changes
|
|
97
97
|
- **Output**: Branch name + line additions/deletions
|
|
98
98
|
- **Display**: `main* (+123 -45)` with color coding
|
|
99
99
|
|
|
100
100
|
### Formatters (`lib/formatters.ts`)
|
|
101
|
+
|
|
101
102
|
- **Colors**: ANSI color codes for terminal output
|
|
102
103
|
- **Token display**: `62.5K`, `1.2M` format
|
|
103
104
|
- **Time formatting**: `3h21m`, `45m` for countdowns
|
|
@@ -106,16 +107,19 @@ Claude Code Hook → stdin JSON → index.ts
|
|
|
106
107
|
## Output Specification
|
|
107
108
|
|
|
108
109
|
### Line 1: Session Info
|
|
110
|
+
|
|
109
111
|
```
|
|
110
112
|
main* (+123 -45) | ~/.claude | Sonnet 4.5
|
|
111
113
|
```
|
|
112
114
|
|
|
113
115
|
### Line 2: Metrics
|
|
116
|
+
|
|
114
117
|
```
|
|
115
118
|
$0.17 (6m) | 62.5K tokens | 31% | 15% (3h27m)
|
|
116
119
|
```
|
|
117
120
|
|
|
118
121
|
**Components:**
|
|
122
|
+
|
|
119
123
|
- `$0.17` - Session cost (USD)
|
|
120
124
|
- `(6m)` - Session duration
|
|
121
125
|
- `62.5K tokens` - Context tokens used (from transcript)
|
|
@@ -147,6 +151,7 @@ echo '{ ... }' | bun run start
|
|
|
147
151
|
### Error Handling & Performance
|
|
148
152
|
|
|
149
153
|
**Error Handling** - All components fail silently:
|
|
154
|
+
|
|
150
155
|
- Missing transcript → 0 tokens, 0%
|
|
151
156
|
- API failure → No usage limits shown
|
|
152
157
|
- Git errors → "no-git" branch
|
|
@@ -155,6 +160,7 @@ echo '{ ... }' | bun run start
|
|
|
155
160
|
This ensures statusline never crashes Claude Code.
|
|
156
161
|
|
|
157
162
|
**Performance Benchmarks:**
|
|
163
|
+
|
|
158
164
|
- Context calculation: ~10-50ms (depends on transcript size)
|
|
159
165
|
- API call: ~100-300ms (cached by Claude API)
|
|
160
166
|
- Git operations: ~20-50ms
|
|
@@ -177,7 +183,23 @@ This ensures statusline never crashes Claude Code.
|
|
|
177
183
|
|
|
178
184
|
## Known Limitations
|
|
179
185
|
|
|
186
|
+
- macOS only (uses Keychain)
|
|
180
187
|
- Requires `git` CLI for git status
|
|
181
188
|
- Requires Claude Code OAuth (not API key)
|
|
182
189
|
- Transcript must be accessible (permissions)
|
|
183
|
-
|
|
190
|
+
|
|
191
|
+
## Critical Requirements
|
|
192
|
+
|
|
193
|
+
### Configuration Updates
|
|
194
|
+
|
|
195
|
+
- **CRITICAL**: When updating `statusline.config.json` or `statusline.config.ts`, you **MUST** update the interactive demo in `src/commands/interactive-config.ts`
|
|
196
|
+
- **ALWAYS** run `bun run config` after config changes to verify the interactive demo works correctly
|
|
197
|
+
- **REQUIRED**: Keep config file structure in sync with interactive prompts
|
|
198
|
+
|
|
199
|
+
### Runtime & Dependencies
|
|
200
|
+
|
|
201
|
+
- **ALWAYS** use Bun for all commands and runtime operations
|
|
202
|
+
- Use `bun run <script>` instead of `npm run` or `pnpm run`
|
|
203
|
+
- Use `bun install` for dependency management
|
|
204
|
+
- **AUTHORIZED LIBRARIES**: `@biomejs/biome` for linting/formatting, third-party libraries like `tiers` are permitted if needed
|
|
205
|
+
- **NEVER** add external npm packages without verification - prefer Bun APIs first
|
|
@@ -9,6 +9,8 @@ Clean, modular statusline for Claude Code with TypeScript + Bun.
|
|
|
9
9
|
- 🧩 Context tokens used
|
|
10
10
|
- 📊 Context percentage (0-100%)
|
|
11
11
|
- ⏱️ Five-hour usage limit with reset time
|
|
12
|
+
- 📅 Weekly usage limit with configurable threshold
|
|
13
|
+
- 📈 Daily usage percentage tracking and statistics
|
|
12
14
|
|
|
13
15
|
## Structure
|
|
14
16
|
|
|
@@ -38,6 +40,12 @@ bun run spend:today
|
|
|
38
40
|
# View this month's spending
|
|
39
41
|
bun run spend:month
|
|
40
42
|
|
|
43
|
+
# View usage statistics
|
|
44
|
+
bun run stats
|
|
45
|
+
|
|
46
|
+
# Interactive config demo
|
|
47
|
+
bun run demo
|
|
48
|
+
|
|
41
49
|
# Format code
|
|
42
50
|
bun run format
|
|
43
51
|
|
|
@@ -45,7 +53,9 @@ bun run format
|
|
|
45
53
|
bun run lint
|
|
46
54
|
```
|
|
47
55
|
|
|
48
|
-
##
|
|
56
|
+
## Tracking Features
|
|
57
|
+
|
|
58
|
+
### Spend Tracking
|
|
49
59
|
|
|
50
60
|
The statusline automatically saves session data to `data/spend.json`. You can view your spending with:
|
|
51
61
|
|
|
@@ -63,6 +73,84 @@ Each session tracks:
|
|
|
63
73
|
- Lines added/removed
|
|
64
74
|
- Working directory
|
|
65
75
|
|
|
76
|
+
### Usage Statistics
|
|
77
|
+
|
|
78
|
+
Daily usage percentages are automatically tracked in `data/daily-usage.json`. Each 5-hour rate limit period is tracked separately using the `resets_at` timestamp as a unique key.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bun run stats
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This shows:
|
|
85
|
+
- Average daily usage percentage across all tracked days
|
|
86
|
+
- Total days and total 5-hour periods tracked
|
|
87
|
+
- Recent 7-day usage history with visual bars
|
|
88
|
+
- Per-day statistics: average, max, min across all 5-hour periods
|
|
89
|
+
- Data is kept for 90 days
|
|
90
|
+
|
|
91
|
+
**How it works:**
|
|
92
|
+
- Each `resets_at` value represents a unique 5-hour rate limit period
|
|
93
|
+
- Multiple 5-hour periods can occur in a single day
|
|
94
|
+
- If the API is called multiple times during the same 5-hour period, only the latest value is kept
|
|
95
|
+
- Daily statistics show the average, maximum, and minimum usage across all periods in that day
|
|
96
|
+
|
|
97
|
+
## Interactive Demo
|
|
98
|
+
|
|
99
|
+
Explore all configuration options with a live preview:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
bun run demo
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This opens an interactive menu where you can:
|
|
106
|
+
- Toggle any config option with arrow keys and spacebar
|
|
107
|
+
- See instant preview of how the statusline changes
|
|
108
|
+
- Navigate through all available settings
|
|
109
|
+
- Reset to defaults with `R`
|
|
110
|
+
- Explore session, limits, weekly usage, and git display options
|
|
111
|
+
|
|
112
|
+
**Controls:**
|
|
113
|
+
- `↑↓` or `j/k` - Navigate options
|
|
114
|
+
- `Space` - Toggle selected option
|
|
115
|
+
- `R` - Reset to defaults
|
|
116
|
+
- `Q` - Quit
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
The statusline can be customized via `statusline.config.ts`. Key configuration options:
|
|
121
|
+
|
|
122
|
+
### Weekly Usage Display
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
weeklyUsage: {
|
|
126
|
+
enabled: boolean | "90%", // true: always show, false: never, "90%": show when 5-hour usage >= 90%
|
|
127
|
+
showTimeLeft: boolean,
|
|
128
|
+
percentage: {
|
|
129
|
+
enabled: boolean,
|
|
130
|
+
progressBar: {
|
|
131
|
+
enabled: boolean,
|
|
132
|
+
length: 5 | 10 | 15,
|
|
133
|
+
style: "filled" | "rectangle" | "braille",
|
|
134
|
+
color: "progressive" | "green" | "yellow" | "red"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Default:** `enabled: "90%"` - Weekly limits appear when your 5-hour usage reaches 90%
|
|
141
|
+
|
|
142
|
+
Display format: `W: ⣿⣿⣧⣀⣀⣀⣀⣀⣀⣀ 45% (6d12h)`
|
|
143
|
+
|
|
144
|
+
### Other Configuration Options
|
|
145
|
+
|
|
146
|
+
- **Session display**: Cost, tokens, context percentage
|
|
147
|
+
- **Limits display**: Five-hour usage limits
|
|
148
|
+
- **Git display**: Branch, changes, staged/unstaged files
|
|
149
|
+
- **Path display**: Full, truncated, or basename modes
|
|
150
|
+
- **Progress bars**: Multiple styles and color schemes
|
|
151
|
+
|
|
152
|
+
See `statusline.config.ts` for all available options and defaults.
|
|
153
|
+
|
|
66
154
|
## Usage in Claude Code
|
|
67
155
|
|
|
68
156
|
Update your `~/.claude/settings.json`:
|