From aeb92ee9d3fdc2b158b3d6a78f5910a2ec820484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20L=C3=B6per?= Date: Sat, 24 Jan 2026 15:13:11 +0100 Subject: [PATCH 1/5] chore: initialize feature branch From b6ce0b1dbbafee605237c07220d9bb487cdae876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20L=C3=B6per?= Date: Sat, 24 Jan 2026 15:43:44 +0100 Subject: [PATCH 2/5] Fix session reset time parsing for Claude CLI v2.1.17 Claude CLI v2.1.17 uses cursor movement ANSI sequences (\x1B[nC] to render text with visual spacing. This causes "Resets" to appear as "Rese s" after ANSI stripping, breaking reset time detection. Changes: - Add cursorForwardPattern regex to match cursor forward sequences - Modify stripANSI to replace cursor movements with spaces before stripping - Add looksLikeResetLine helper to detect garbled reset patterns - Bump VERSION to 2.1.11-4 - Update flake.nix claude-code input to v2.1.17 - Update README compatibility table for 2.1.11-2.1.17 Co-Authored-By: Claude Opus 4.5 --- README.md | 2 +- VERSION | 2 +- flake.nix | 2 +- main.go | 38 +++++++++++++++++++++++++++++++++++++- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a540142..a5ae08b 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ 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.11-X | 2.1.11 - 2.1.17 | 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..4ce1228 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.11-3 +2.1.11-4 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..a0dbc4b 100644 --- a/main.go +++ b/main.go @@ -136,6 +136,10 @@ 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) + // 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 +202,20 @@ 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 { + n, _ := strconv.Atoi(matches[1]) + if n > 0 && n <= 10 { // Reasonable limit to avoid memory issues + return strings.Repeat(" ", n) + } + } + return " " // Default single space + }) + // Then strip remaining ANSI codes return ansiPattern.ReplaceAllString(text, "") } @@ -441,6 +459,24 @@ 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"). +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" (missing 't' due to cursor movements) + // 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 +492,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 From ddbc62ec7146bf00bd0a9dba520babf8d390f163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20L=C3=B6per?= Date: Sat, 24 Jan 2026 15:56:45 +0100 Subject: [PATCH 3/5] Address Copilot review feedback - Fix cursor forward handling: n=0 returns empty string, increase limit to 100 - Bump VERSION to 2.1.17-1 to match flake.nix claude-code version - Add lowercase documentation comment to looksLikeResetLine function - Update README compatibility table for 2.1.17-X Co-Authored-By: Claude Opus 4.5 --- README.md | 2 +- VERSION | 2 +- flake.lock | 8 ++++---- main.go | 13 +++++++++++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a5ae08b..b971317 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ 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 - 2.1.17 | Tested :white_check_mark: | +| 2.1.17-X | 2.1.11 - 2.1.17 | 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 4ce1228..f6e132b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.11-4 +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/main.go b/main.go index a0dbc4b..2469050 100644 --- a/main.go +++ b/main.go @@ -209,11 +209,19 @@ func stripANSI(text string) string { matches := cursorForwardPattern.FindStringSubmatch(match) if len(matches) > 1 { n, _ := strconv.Atoi(matches[1]) - if n > 0 && n <= 10 { // Reasonable limit to avoid memory issues + // Model cursor movement: 0 -> no space, >0 -> proportional spaces with a safe upper bound + if n == 0 { + return "" + } + if n > 0 { + const maxSpaces = 100 // Reasonable limit to avoid memory issues + if n > maxSpaces { + n = maxSpaces + } return strings.Repeat(" ", n) } } - return " " // Default single space + return " " // Default single space for malformed sequences }) // Then strip remaining ANSI codes return ansiPattern.ReplaceAllString(text, "") @@ -462,6 +470,7 @@ func isQuotaSectionMarker(lineLower string) bool { // 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") { From 9ba9d4cd20b1ec25748cec66d7889e5886e70b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20L=C3=B6per?= Date: Sat, 24 Jan 2026 16:13:19 +0100 Subject: [PATCH 4/5] Address second round of Copilot review feedback - Handle \x1B[C (no digit) as cursor forward 1 per ANSI standard - Remove redundant 'if n > 0' check after n == 0 early return - Clarify comment about cursor movements affecting any character position Co-Authored-By: Claude Opus 4.5 --- main.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index 2469050..5a2d37d 100644 --- a/main.go +++ b/main.go @@ -137,8 +137,9 @@ var ( 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`) + cursorForwardPattern = regexp.MustCompile(`\x1B\[(\d*)C`) // Account type patterns (case insensitive) // v2.1.x format: "Claude Max" without leading · @@ -208,18 +209,20 @@ func stripANSI(text string) string { text = cursorForwardPattern.ReplaceAllStringFunc(text, func(match string) string { matches := cursorForwardPattern.FindStringSubmatch(match) if len(matches) > 1 { - n, _ := strconv.Atoi(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 "" } - if n > 0 { - const maxSpaces = 100 // Reasonable limit to avoid memory issues - if n > maxSpaces { - n = maxSpaces - } - return strings.Repeat(" ", n) + 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 }) @@ -477,7 +480,8 @@ func looksLikeResetLine(lineLower string) bool { return true } // Garbled patterns from cursor movement artifacts in Claude CLI v2.1.17+ - // The word "Resets" may be rendered as "Rese s" (missing 't' due to cursor movements) + // 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")) { From a62c844d735958c2ec57a9531566d12703026693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20L=C3=B6per?= Date: Sat, 24 Jan 2026 16:28:58 +0100 Subject: [PATCH 5/5] Fix README compatibility table format Keep separate rows for 2.1.11-X and 2.1.17-X versions. Co-Authored-By: Claude Opus 4.5 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b971317..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.17-X | 2.1.11 - 2.1.17 | 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: |