aiblueprint-cli 1.1.8 → 1.2.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.
Files changed (41) hide show
  1. package/README.md +38 -0
  2. package/claude-code-config/scripts/command-validator/README.md +147 -0
  3. package/claude-code-config/scripts/command-validator/biome.json +29 -0
  4. package/claude-code-config/scripts/command-validator/bun.lockb +0 -0
  5. package/claude-code-config/scripts/command-validator/dist/cli.js +544 -0
  6. package/claude-code-config/scripts/command-validator/package.json +27 -0
  7. package/claude-code-config/scripts/command-validator/src/__tests__/validator.test.ts +148 -0
  8. package/claude-code-config/scripts/command-validator/src/cli.ts +118 -0
  9. package/claude-code-config/scripts/command-validator/src/lib/security-rules.ts +172 -0
  10. package/claude-code-config/scripts/command-validator/src/lib/types.ts +33 -0
  11. package/claude-code-config/scripts/command-validator/src/lib/validator.ts +360 -0
  12. package/claude-code-config/scripts/command-validator/vitest.config.ts +7 -0
  13. package/claude-code-config/scripts/statusline/package.json +1 -3
  14. package/claude-code-config/scripts/statusline/src/index.ts +5 -107
  15. package/claude-code-config/scripts/statusline/src/lib/context.ts +66 -87
  16. package/claude-code-config/scripts/statusline/src/lib/formatters.ts +16 -186
  17. package/claude-code-config/scripts/statusline/statusline.config.ts +4 -101
  18. package/dist/cli.js +951 -12
  19. package/package.json +1 -1
  20. package/claude-code-config/agents/fix-grammar.md +0 -49
  21. package/claude-code-config/agents/snipper.md +0 -36
  22. package/claude-code-config/commands/claude-memory.md +0 -190
  23. package/claude-code-config/commands/cleanup-context.md +0 -82
  24. package/claude-code-config/commands/debug.md +0 -91
  25. package/claude-code-config/commands/deep-code-analysis.md +0 -87
  26. package/claude-code-config/commands/epct/code.md +0 -171
  27. package/claude-code-config/commands/epct/deploy.md +0 -116
  28. package/claude-code-config/commands/epct/explore.md +0 -97
  29. package/claude-code-config/commands/epct/plan.md +0 -132
  30. package/claude-code-config/commands/epct/tasks.md +0 -206
  31. package/claude-code-config/commands/explain-architecture.md +0 -113
  32. package/claude-code-config/commands/melvynx-plugin.md +0 -1
  33. package/claude-code-config/commands/prompt-agent.md +0 -126
  34. package/claude-code-config/commands/prompt-command.md +0 -225
  35. package/claude-code-config/scripts/statusline/data/.gitignore +0 -5
  36. package/claude-code-config/scripts/statusline/src/commands/CLAUDE.md +0 -3
  37. package/claude-code-config/scripts/statusline/src/commands/spend-month.ts +0 -60
  38. package/claude-code-config/scripts/statusline/src/commands/spend-today.ts +0 -42
  39. package/claude-code-config/scripts/statusline/src/lib/git.ts +0 -100
  40. package/claude-code-config/scripts/statusline/src/lib/spend.ts +0 -119
  41. 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
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ },
7
+ });
@@ -1,13 +1,11 @@
1
1
  {
2
2
  "name": "statusline",
3
- "version": "1.0.0",
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 firstLine = buildFirstLine(
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
- if (defaultConfig.oneLine) {
125
- const sep = ` ${colors.GRAY}${defaultConfig.separator}${colors.LIGHT_GRAY} `;
126
- console.log(`${firstLine}${sep}${secondLine}`);
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
- export interface TokenUsage {
4
- input_tokens: number;
5
- output_tokens: number;
6
- cache_creation_input_tokens?: number;
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
- export interface TranscriptLine {
11
- message?: { usage?: TokenUsage };
12
- timestamp?: string;
13
- isSidechain?: boolean;
14
- isApiErrorMessage?: boolean;
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
- tokens: number;
19
- percentage: number;
17
+ tokens: number;
18
+ percentage: number;
20
19
  }
21
20
 
22
- export async function getContextLength(
23
- transcriptPath: string,
24
- ): Promise<number> {
25
- try {
26
- const content = await Bun.file(transcriptPath).text();
27
- const lines = content.trim().split("\n");
28
-
29
- if (lines.length === 0) return 0;
30
-
31
- let mostRecentMainChainEntry: TranscriptLine | null = null;
32
- let mostRecentTimestamp: Date | null = null;
33
-
34
- for (const line of lines) {
35
- try {
36
- const data = JSON.parse(line) as TranscriptLine;
37
-
38
- if (!data.message?.usage) continue;
39
- if (data.isSidechain === true) continue;
40
- if (data.isApiErrorMessage === true) continue;
41
- if (!data.timestamp) continue;
42
-
43
- const entryTime = new Date(data.timestamp);
44
-
45
- if (!mostRecentTimestamp || entryTime > mostRecentTimestamp) {
46
- mostRecentTimestamp = entryTime;
47
- mostRecentMainChainEntry = data;
48
- }
49
- } catch {}
50
- }
51
-
52
- if (!mostRecentMainChainEntry?.message?.usage) {
53
- return 0;
54
- }
55
-
56
- const usage = mostRecentMainChainEntry.message.usage;
57
-
58
- return (
59
- (usage.input_tokens || 0) +
60
- (usage.cache_read_input_tokens ?? 0) +
61
- (usage.cache_creation_input_tokens ?? 0)
62
- );
63
- } catch {
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
- export interface ContextDataParams {
69
- transcriptPath: string;
70
- maxContextTokens: number;
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
- transcriptPath,
78
- maxContextTokens,
79
- autocompactBufferTokens,
80
- useUsableContextOnly = false,
81
- overheadTokens = 0,
71
+ transcriptPath,
72
+ maxContextTokens,
82
73
  }: ContextDataParams): Promise<ContextResult> {
83
- if (!transcriptPath || !existsSync(transcriptPath)) {
84
- return { tokens: 0, percentage: 0 };
85
- }
86
-
87
- const contextLength = await getContextLength(transcriptPath);
88
- let totalTokens = contextLength + overheadTokens;
89
-
90
- // If useUsableContextOnly is true, add the autocompact buffer to displayed tokens
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
  }