Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.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: |

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.1.11-3
2.1.17-1
Comment thread
MartinLoeper marked this conversation as resolved.
8 changes: 4 additions & 4 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Comment thread
MartinLoeper marked this conversation as resolved.
inputs.nixpkgs.follows = "nixpkgs";
};
};
Expand Down
51 changes: 50 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ·
Expand Down Expand Up @@ -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, "")
}

Expand Down Expand Up @@ -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").
Comment thread
MartinLoeper marked this conversation as resolved.
// 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
}
Comment thread
MartinLoeper marked this conversation as resolved.
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
Expand All @@ -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

Expand Down
Loading