diff --git a/README.md b/README.md index a540142..35c6b36 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ claude-o-meter scrapes output from the Claude CLI, so it depends on specific Cla | claude-o-meter | Claude Code | Status | |----------------|-------------|-----------| -| 2.1.11-X | 2.1.11 | Tested :white_check_mark: | +| 2.1.17-X | 2.1.17 | Tested :white_check_mark: | +| 2.1.11-X | 2.1.11 - 2.1.16 | Tested :white_check_mark: | | 2.1.1-X | 2.1.0, 2.1.1 | Tested :white_check_mark: | | 2.0.76-X | 2.0.76 | Tested :white_check_mark: | diff --git a/VERSION b/VERSION index 339fdde..f6e132b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.11-3 +2.1.17-1 diff --git a/flake.lock b/flake.lock index b23709e..c81e1bf 100644 --- a/flake.lock +++ b/flake.lock @@ -8,16 +8,16 @@ ] }, "locked": { - "lastModified": 1768617850, - "narHash": "sha256-dEqrnk0nld4hmdt9sqQfWEknbLDD2PD6P8J7mA60DxE=", + "lastModified": 1769119935, + "narHash": "sha256-yzOuo4PAG/OdOcO20xYkBxFJ2VA0gCT7ESH0t0Uevec=", "owner": "sadjow", "repo": "claude-code-nix", - "rev": "b125155ec32fb00eacd6ba1b34a1be6c9d127a8e", + "rev": "7b65d03098631243c1ab7195101996b9c73c7509", "type": "github" }, "original": { "owner": "sadjow", - "ref": "v2.1.11", + "ref": "v2.1.17", "repo": "claude-code-nix", "type": "github" } diff --git a/flake.nix b/flake.nix index 0cdae42..cd1dd8a 100644 --- a/flake.nix +++ b/flake.nix @@ -5,7 +5,7 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; claude-code = { - url = "github:sadjow/claude-code-nix?ref=v2.1.11"; + url = "github:sadjow/claude-code-nix?ref=v2.1.17"; inputs.nixpkgs.follows = "nixpkgs"; }; }; diff --git a/main.go b/main.go index 462ef93..5a2d37d 100644 --- a/main.go +++ b/main.go @@ -136,6 +136,11 @@ var ( // OSC: \x1B] followed by text and terminated by BEL (\x07) or ST (\x1B\\) ansiPattern = regexp.MustCompile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07\x1B]*(?:\x07|\x1B\\))`) + // Cursor movement pattern: \x1B[nC (cursor forward n positions) + // Also handles \x1B[C (no digit) which means forward 1 position per ANSI standard + // Used to replace cursor movements with spaces to preserve word boundaries + cursorForwardPattern = regexp.MustCompile(`\x1B\[(\d*)C`) + // Account type patterns (case insensitive) // v2.1.x format: "Claude Max" without leading · // v2.0.x format: "· claude max" with leading · @@ -198,6 +203,30 @@ var ( ) func stripANSI(text string) string { + // First, replace cursor forward sequences with appropriate spaces + // This preserves word boundaries that the terminal would display + // Claude CLI v2.1.17 uses \x1B[nC to render text with visual spacing + text = cursorForwardPattern.ReplaceAllStringFunc(text, func(match string) string { + matches := cursorForwardPattern.FindStringSubmatch(match) + if len(matches) > 1 { + // Empty string means no digit was provided, default to 1 per ANSI standard + n := 1 + if matches[1] != "" { + n, _ = strconv.Atoi(matches[1]) + } + // Model cursor movement: 0 -> no space, >0 -> proportional spaces with a safe upper bound + if n == 0 { + return "" + } + const maxSpaces = 100 // Reasonable limit to avoid memory issues + if n > maxSpaces { + n = maxSpaces + } + return strings.Repeat(" ", n) + } + return " " // Default single space for malformed sequences + }) + // Then strip remaining ANSI codes return ansiPattern.ReplaceAllString(text, "") } @@ -441,6 +470,26 @@ func isQuotaSectionMarker(lineLower string) bool { return false } +// looksLikeResetLine checks if a line appears to be a reset time line. +// Handles both normal "reset"/"renew" keywords and garbled text from +// cursor movement artifacts (e.g., "rese s" instead of "resets"). +// The input should already be lowercase for efficiency. +func looksLikeResetLine(lineLower string) bool { + // Standard keywords + if strings.Contains(lineLower, "reset") || strings.Contains(lineLower, "renew") { + return true + } + // Garbled patterns from cursor movement artifacts in Claude CLI v2.1.17+ + // The word "Resets" may be rendered as "Rese s" where cursor movement escape + // sequences create gaps in the word and can affect any character position. + // Look for "rese" followed by a time indicator (am/pm) + if strings.Contains(lineLower, "rese") && + (strings.Contains(lineLower, "am") || strings.Contains(lineLower, "pm")) { + return true + } + return false +} + func parseResetTime(lines []string, startIdx int) (string, *time.Time, *int64) { // Look within next 14 lines for reset information, but stop if we hit another quota section endIdx := startIdx + 14 @@ -456,7 +505,7 @@ func parseResetTime(lines []string, startIdx int) (string, *time.Time, *int64) { break } - if strings.Contains(line, "reset") || strings.Contains(line, "renew") { + if looksLikeResetLine(line) { // First try parsing relative duration components var totalSeconds int64