aiblueprint-cli 1.1.8 → 1.2.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-code-config/scripts/command-validator/README.md +147 -0
- package/claude-code-config/scripts/command-validator/biome.json +29 -0
- package/claude-code-config/scripts/command-validator/bun.lockb +0 -0
- package/claude-code-config/scripts/command-validator/dist/cli.js +544 -0
- package/claude-code-config/scripts/command-validator/package.json +27 -0
- package/claude-code-config/scripts/command-validator/src/__tests__/validator.test.ts +148 -0
- package/claude-code-config/scripts/command-validator/src/cli.ts +118 -0
- package/claude-code-config/scripts/command-validator/src/lib/security-rules.ts +172 -0
- package/claude-code-config/scripts/command-validator/src/lib/types.ts +33 -0
- package/claude-code-config/scripts/command-validator/src/lib/validator.ts +360 -0
- package/claude-code-config/scripts/command-validator/vitest.config.ts +7 -0
- package/claude-code-config/scripts/statusline/package.json +1 -3
- package/claude-code-config/scripts/statusline/src/index.ts +5 -107
- package/claude-code-config/scripts/statusline/src/lib/context.ts +66 -87
- package/claude-code-config/scripts/statusline/src/lib/formatters.ts +16 -186
- package/claude-code-config/scripts/statusline/statusline.config.ts +4 -101
- package/dist/cli.js +938 -12
- package/package.json +1 -1
- package/claude-code-config/agents/fix-grammar.md +0 -49
- package/claude-code-config/agents/snipper.md +0 -36
- package/claude-code-config/commands/claude-memory.md +0 -190
- package/claude-code-config/commands/cleanup-context.md +0 -82
- package/claude-code-config/commands/debug.md +0 -91
- package/claude-code-config/commands/deep-code-analysis.md +0 -87
- package/claude-code-config/commands/epct/code.md +0 -171
- package/claude-code-config/commands/epct/deploy.md +0 -116
- package/claude-code-config/commands/epct/explore.md +0 -97
- package/claude-code-config/commands/epct/plan.md +0 -132
- package/claude-code-config/commands/epct/tasks.md +0 -206
- package/claude-code-config/commands/explain-architecture.md +0 -113
- package/claude-code-config/commands/melvynx-plugin.md +0 -1
- package/claude-code-config/commands/prompt-agent.md +0 -126
- package/claude-code-config/commands/prompt-command.md +0 -225
- package/claude-code-config/scripts/statusline/data/.gitignore +0 -5
- package/claude-code-config/scripts/statusline/src/commands/CLAUDE.md +0 -3
- package/claude-code-config/scripts/statusline/src/commands/spend-month.ts +0 -60
- package/claude-code-config/scripts/statusline/src/commands/spend-today.ts +0 -42
- package/claude-code-config/scripts/statusline/src/lib/git.ts +0 -100
- package/claude-code-config/scripts/statusline/src/lib/spend.ts +0 -119
- package/claude-code-config/scripts/statusline/src/lib/usage-limits.ts +0 -147
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { SAFE_COMMANDS, SECURITY_RULES } from "./security-rules";
|
|
2
|
+
import type { ValidationResult } from "./types";
|
|
3
|
+
|
|
4
|
+
export class CommandValidator {
|
|
5
|
+
validate(command: string, toolName = "Unknown"): ValidationResult {
|
|
6
|
+
const result: ValidationResult = {
|
|
7
|
+
isValid: true,
|
|
8
|
+
severity: "LOW",
|
|
9
|
+
violations: [],
|
|
10
|
+
sanitizedCommand: command,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
if (!command || typeof command !== "string") {
|
|
14
|
+
result.isValid = false;
|
|
15
|
+
result.violations.push("Invalid command format");
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (command.length > 2000) {
|
|
20
|
+
result.isValid = false;
|
|
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)) {
|
|
50
|
+
result.isValid = false;
|
|
51
|
+
result.severity = "CRITICAL";
|
|
52
|
+
result.violations.push(`Critical dangerous command: ${mainCommand}`);
|
|
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
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const path of SECURITY_RULES.PROTECTED_PATHS) {
|
|
136
|
+
if (command.includes(path)) {
|
|
137
|
+
if (
|
|
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)) {
|
|
197
|
+
result.isValid = false;
|
|
198
|
+
result.severity = "HIGH";
|
|
199
|
+
result.violations.push("Binary or encoded content detected");
|
|
200
|
+
return result;
|
|
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") {
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
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
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
splitCommandChain(command: string): string[] {
|
|
294
|
+
const commands: string[] = [];
|
|
295
|
+
let current = "";
|
|
296
|
+
let inQuotes = false;
|
|
297
|
+
let quoteChar = "";
|
|
298
|
+
|
|
299
|
+
for (let i = 0; i < command.length; i++) {
|
|
300
|
+
const char = command[i];
|
|
301
|
+
const nextChar = command[i + 1];
|
|
302
|
+
|
|
303
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
304
|
+
inQuotes = true;
|
|
305
|
+
quoteChar = char;
|
|
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;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (current.trim()) {
|
|
330
|
+
commands.push(current.trim());
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return commands.filter((cmd) => cmd.length > 0);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
isRmRfCommandSafe(command: string): boolean {
|
|
337
|
+
const rmRfMatch = command.match(/rm\s+.*-rf\s+([^\s;&|]+)/);
|
|
338
|
+
if (!rmRfMatch) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const targetPath = rmRfMatch[1];
|
|
343
|
+
|
|
344
|
+
if (targetPath === "/" || targetPath.endsWith("/")) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for (const safePath of SECURITY_RULES.SAFE_RM_PATHS) {
|
|
349
|
+
if (targetPath.startsWith(safePath)) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!targetPath.startsWith("/")) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "statusline",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "bun run src/index.ts",
|
|
8
8
|
"test": "bun run test.ts",
|
|
9
|
-
"spend:today": "bun run src/commands/spend-today.ts",
|
|
10
|
-
"spend:month": "bun run src/commands/spend-month.ts",
|
|
11
9
|
"lint": "biome check --write .",
|
|
12
10
|
"format": "biome format --write ."
|
|
13
11
|
},
|
|
@@ -1,89 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import type { StatuslineConfig } from "../statusline.config";
|
|
4
3
|
import { defaultConfig } from "../statusline.config";
|
|
5
4
|
import { getContextData } from "./lib/context";
|
|
6
|
-
import {
|
|
7
|
-
colors,
|
|
8
|
-
formatBranch,
|
|
9
|
-
formatCost,
|
|
10
|
-
formatDuration,
|
|
11
|
-
formatPath,
|
|
12
|
-
formatProgressBar,
|
|
13
|
-
formatResetTime,
|
|
14
|
-
formatSession,
|
|
15
|
-
} from "./lib/formatters";
|
|
16
|
-
import { getGitStatus } from "./lib/git";
|
|
17
|
-
import { saveSession } from "./lib/spend";
|
|
5
|
+
import { colors, formatPath, formatSession } from "./lib/formatters";
|
|
18
6
|
import type { HookInput } from "./lib/types";
|
|
19
|
-
import { getUsageLimits } from "./lib/usage-limits";
|
|
20
|
-
|
|
21
|
-
function buildFirstLine(
|
|
22
|
-
branch: string,
|
|
23
|
-
dirPath: string,
|
|
24
|
-
modelName: string,
|
|
25
|
-
showSonnetModel: boolean,
|
|
26
|
-
separator: string,
|
|
27
|
-
): string {
|
|
28
|
-
const isSonnet = modelName.toLowerCase().includes("sonnet");
|
|
29
|
-
const sep = `${colors.GRAY}${separator}${colors.LIGHT_GRAY}`;
|
|
30
|
-
|
|
31
|
-
if (isSonnet && !showSonnetModel) {
|
|
32
|
-
return `${colors.LIGHT_GRAY}${branch} ${sep} ${dirPath}${colors.RESET}`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return `${colors.LIGHT_GRAY}${branch} ${sep} ${dirPath} ${sep} ${modelName}${colors.RESET}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function buildSecondLine(
|
|
39
|
-
sessionCost: string,
|
|
40
|
-
_sessionDuration: string,
|
|
41
|
-
tokensUsed: number,
|
|
42
|
-
tokensMax: number,
|
|
43
|
-
contextPercentage: number,
|
|
44
|
-
fiveHourUtilization: number | null,
|
|
45
|
-
fiveHourReset: string | null,
|
|
46
|
-
sessionConfig: StatuslineConfig["session"],
|
|
47
|
-
limitsConfig: StatuslineConfig["limits"],
|
|
48
|
-
separator: string,
|
|
49
|
-
): string {
|
|
50
|
-
let line = formatSession(
|
|
51
|
-
sessionCost,
|
|
52
|
-
tokensUsed,
|
|
53
|
-
tokensMax,
|
|
54
|
-
contextPercentage,
|
|
55
|
-
sessionConfig,
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
if (fiveHourUtilization !== null && fiveHourReset) {
|
|
59
|
-
const resetTime = formatResetTime(fiveHourReset);
|
|
60
|
-
const sep = `${colors.GRAY}${separator}`;
|
|
61
|
-
|
|
62
|
-
if (limitsConfig.showProgressBar) {
|
|
63
|
-
const bar = formatProgressBar(
|
|
64
|
-
fiveHourUtilization,
|
|
65
|
-
limitsConfig.progressBarLength,
|
|
66
|
-
limitsConfig.color,
|
|
67
|
-
);
|
|
68
|
-
line += ` ${sep} L: ${bar} ${colors.LIGHT_GRAY}${fiveHourUtilization}${colors.GRAY}% ${colors.GRAY}(${resetTime} left)`;
|
|
69
|
-
} else {
|
|
70
|
-
line += ` ${sep} L:${colors.LIGHT_GRAY} ${fiveHourUtilization}${colors.GRAY}% ${colors.GRAY}(${resetTime} left)`;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
line += colors.RESET;
|
|
75
|
-
|
|
76
|
-
return line;
|
|
77
|
-
}
|
|
78
7
|
|
|
79
8
|
async function main() {
|
|
80
9
|
try {
|
|
81
10
|
const input: HookInput = await Bun.stdin.json();
|
|
82
11
|
|
|
83
|
-
await saveSession(input);
|
|
84
|
-
|
|
85
|
-
const git = await getGitStatus();
|
|
86
|
-
const branch = formatBranch(git, defaultConfig.git);
|
|
87
12
|
const dirPath = formatPath(
|
|
88
13
|
input.workspace.current_dir,
|
|
89
14
|
defaultConfig.pathDisplayMode,
|
|
@@ -92,49 +17,22 @@ async function main() {
|
|
|
92
17
|
const contextData = await getContextData({
|
|
93
18
|
transcriptPath: input.transcript_path,
|
|
94
19
|
maxContextTokens: defaultConfig.context.maxContextTokens,
|
|
95
|
-
autocompactBufferTokens: defaultConfig.context.autocompactBufferTokens,
|
|
96
|
-
useUsableContextOnly: defaultConfig.context.useUsableContextOnly,
|
|
97
|
-
overheadTokens: defaultConfig.context.overheadTokens,
|
|
98
20
|
});
|
|
99
|
-
const usageLimits = await getUsageLimits();
|
|
100
|
-
|
|
101
|
-
const sessionCost = formatCost(input.cost.total_cost_usd);
|
|
102
|
-
const sessionDuration = formatDuration(input.cost.total_duration_ms);
|
|
103
21
|
|
|
104
|
-
const
|
|
105
|
-
branch,
|
|
106
|
-
dirPath,
|
|
107
|
-
input.model.display_name,
|
|
108
|
-
defaultConfig.showSonnetModel,
|
|
109
|
-
defaultConfig.separator,
|
|
110
|
-
);
|
|
111
|
-
const secondLine = buildSecondLine(
|
|
112
|
-
sessionCost,
|
|
113
|
-
sessionDuration,
|
|
22
|
+
const sessionInfo = formatSession(
|
|
114
23
|
contextData.tokens,
|
|
115
|
-
defaultConfig.context.maxContextTokens,
|
|
116
24
|
contextData.percentage,
|
|
117
|
-
usageLimits.five_hour?.utilization ?? null,
|
|
118
|
-
usageLimits.five_hour?.resets_at ?? null,
|
|
119
25
|
defaultConfig.session,
|
|
120
|
-
defaultConfig.limits,
|
|
121
|
-
defaultConfig.separator,
|
|
122
26
|
);
|
|
123
27
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
console.log(""); // Empty second line for spacing
|
|
128
|
-
} else {
|
|
129
|
-
console.log(firstLine);
|
|
130
|
-
console.log(secondLine);
|
|
131
|
-
}
|
|
28
|
+
const sep = ` ${colors.GRAY}${defaultConfig.separator}${colors.LIGHT_GRAY} `;
|
|
29
|
+
console.log(`${colors.LIGHT_GRAY}${dirPath}${sep}${sessionInfo}${colors.RESET}`);
|
|
30
|
+
console.log("");
|
|
132
31
|
} catch (error) {
|
|
133
32
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
134
33
|
console.log(
|
|
135
34
|
`${colors.RED}Error:${colors.LIGHT_GRAY} ${errorMessage}${colors.RESET}`,
|
|
136
35
|
);
|
|
137
|
-
console.log(`${colors.GRAY}Check statusline configuration${colors.RESET}`);
|
|
138
36
|
}
|
|
139
37
|
}
|
|
140
38
|
|
|
@@ -1,103 +1,82 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
cache_read_input_tokens?: number;
|
|
3
|
+
interface TokenUsage {
|
|
4
|
+
input_tokens: number;
|
|
5
|
+
cache_creation_input_tokens?: number;
|
|
6
|
+
cache_read_input_tokens?: number;
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
interface TranscriptLine {
|
|
10
|
+
message?: { usage?: TokenUsage };
|
|
11
|
+
timestamp?: string;
|
|
12
|
+
isSidechain?: boolean;
|
|
13
|
+
isApiErrorMessage?: boolean;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
export interface ContextResult {
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
tokens: number;
|
|
18
|
+
percentage: number;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return 0;
|
|
65
|
-
}
|
|
21
|
+
async function getContextLength(transcriptPath: string): Promise<number> {
|
|
22
|
+
try {
|
|
23
|
+
const content = await Bun.file(transcriptPath).text();
|
|
24
|
+
const lines = content.trim().split("\n");
|
|
25
|
+
|
|
26
|
+
if (lines.length === 0) return 0;
|
|
27
|
+
|
|
28
|
+
let mostRecentEntry: TranscriptLine | null = null;
|
|
29
|
+
let mostRecentTimestamp: Date | null = null;
|
|
30
|
+
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
try {
|
|
33
|
+
const data = JSON.parse(line) as TranscriptLine;
|
|
34
|
+
|
|
35
|
+
if (!data.message?.usage) continue;
|
|
36
|
+
if (data.isSidechain === true) continue;
|
|
37
|
+
if (data.isApiErrorMessage === true) continue;
|
|
38
|
+
if (!data.timestamp) continue;
|
|
39
|
+
|
|
40
|
+
const entryTime = new Date(data.timestamp);
|
|
41
|
+
|
|
42
|
+
if (!mostRecentTimestamp || entryTime > mostRecentTimestamp) {
|
|
43
|
+
mostRecentTimestamp = entryTime;
|
|
44
|
+
mostRecentEntry = data;
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!mostRecentEntry?.message?.usage) {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const usage = mostRecentEntry.message.usage;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
(usage.input_tokens || 0) +
|
|
57
|
+
(usage.cache_read_input_tokens ?? 0) +
|
|
58
|
+
(usage.cache_creation_input_tokens ?? 0)
|
|
59
|
+
);
|
|
60
|
+
} catch {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
autocompactBufferTokens: number;
|
|
72
|
-
useUsableContextOnly?: boolean;
|
|
73
|
-
overheadTokens?: number;
|
|
65
|
+
interface ContextDataParams {
|
|
66
|
+
transcriptPath: string;
|
|
67
|
+
maxContextTokens: number;
|
|
74
68
|
}
|
|
75
69
|
|
|
76
70
|
export async function getContextData({
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
autocompactBufferTokens,
|
|
80
|
-
useUsableContextOnly = false,
|
|
81
|
-
overheadTokens = 0,
|
|
71
|
+
transcriptPath,
|
|
72
|
+
maxContextTokens,
|
|
82
73
|
}: ContextDataParams): Promise<ContextResult> {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (useUsableContextOnly) {
|
|
92
|
-
totalTokens += autocompactBufferTokens;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Always calculate percentage based on max context window
|
|
96
|
-
// (matching /context display behavior)
|
|
97
|
-
const percentage = Math.min(100, (totalTokens / maxContextTokens) * 100);
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
tokens: totalTokens,
|
|
101
|
-
percentage: Math.round(percentage),
|
|
102
|
-
};
|
|
74
|
+
if (!transcriptPath || !existsSync(transcriptPath)) {
|
|
75
|
+
return { tokens: 0, percentage: 0 };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const tokens = await getContextLength(transcriptPath);
|
|
79
|
+
const percentage = Math.min(100, Math.round((tokens / maxContextTokens) * 100));
|
|
80
|
+
|
|
81
|
+
return { tokens, percentage };
|
|
103
82
|
}
|