quotacap 0.0.27 → 0.0.28

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.
@@ -35,35 +35,73 @@ function parseGrokReset(raw, now) {
35
35
  }
36
36
  return dt.toISOString();
37
37
  }
38
+ function validPct(v) {
39
+ return Number.isFinite(v) && v >= 0 && v <= 100;
40
+ }
38
41
  export function parseGrokTui(text, now = new Date()) {
39
42
  const cleaned = stripAnsi(text);
40
- const planMatch = cleaned.match(/Weekly lim[i!l]t\s*\(([^)]+)\)/i);
43
+ // The TUI writes "Weekly lim", repositions the cursor, then writes
44
+ // "t (Tier)", so after ANSI stripping the header reads "Weekly limt".
45
+ // The [i!l] class stays optional to keep tolerating past glitch variants.
46
+ const headerRe = /Weekly\s+lim[i!l]?t\s*\(([^)]+)\)/gi;
41
47
  let plan = "unknown";
42
- if (planMatch) {
43
- plan = planMatch[1].trim();
48
+ let lastHeaderIdx = -1;
49
+ for (const m of cleaned.matchAll(headerRe)) {
50
+ if (m.index === undefined)
51
+ continue;
52
+ plan = m[1].trim();
53
+ lastHeaderIdx = m.index;
44
54
  }
45
- else {
55
+ if (lastHeaderIdx < 0) {
46
56
  const tier = cleaned.match(/\b(SuperGrok|Grok\s+Pro|Grok\s+Enterprise|Grok\s+Basic)\b/i);
47
57
  if (tier)
48
58
  plan = tier[1].trim();
49
59
  }
50
- const pctRe = /Weekly lim[i!l]t[^\n%]*?(\d+)%/i;
51
- const mPct = cleaned.match(pctRe);
52
60
  let usedPct = null;
53
- if (mPct) {
54
- const v = parseInt(mPct[1], 10);
55
- if (Number.isFinite(v) && v >= 0 && v <= 100)
56
- usedPct = v;
61
+ // 1. Dialog header: first percent in a bounded window after the LAST
62
+ // header (later renders supersede earlier ones). The bar percent sits on
63
+ // the line below the header, so this must span newlines.
64
+ if (lastHeaderIdx >= 0) {
65
+ const after = cleaned.slice(lastHeaderIdx, lastHeaderIdx + 1200);
66
+ const m = after.match(/(\d{1,3})%/);
67
+ if (m) {
68
+ const v = parseInt(m[1], 10);
69
+ if (validPct(v))
70
+ usedPct = v;
71
+ }
57
72
  }
73
+ // 2. Legacy same-line form ("Weekly limit (Tier) 26%"), excluding the
74
+ // startup status line "Weekly limit left: N%", which reports REMAINING
75
+ // quota with inverted semantics. Last valid match wins.
58
76
  if (usedPct === null) {
59
- const barRe = /[█░▓▓]+\s*(\d+)%/;
60
- const b = cleaned.match(barRe);
61
- if (b) {
62
- const v = parseInt(b[1], 10);
63
- if (Number.isFinite(v) && v >= 0 && v <= 100)
77
+ const lineRe = /Weekly\s+lim[i!l]?t[^\n%]*?(\d{1,3})%/gi;
78
+ for (const m of cleaned.matchAll(lineRe)) {
79
+ if (/left/i.test(m[0]))
80
+ continue;
81
+ const v = parseInt(m[1], 10);
82
+ if (validPct(v))
64
83
  usedPct = v;
65
84
  }
66
85
  }
86
+ // 3. Bare progress-bar percent. Last valid match wins.
87
+ if (usedPct === null) {
88
+ const barRe = /[█░▓▒]+\s*(\d{1,3})%/g;
89
+ for (const m of cleaned.matchAll(barRe)) {
90
+ const v = parseInt(m[1], 10);
91
+ if (validPct(v))
92
+ usedPct = v;
93
+ }
94
+ }
95
+ // 4. Status-line remaining figure ("Weekly limit left: N%") converted to
96
+ // used. Last valid match wins.
97
+ if (usedPct === null) {
98
+ const leftRe = /Weekly\s+lim[i!l]?t\s+left\s*:?\s*(\d{1,3})%/gi;
99
+ for (const m of cleaned.matchAll(leftRe)) {
100
+ const v = parseInt(m[1], 10);
101
+ if (validPct(v))
102
+ usedPct = 100 - v;
103
+ }
104
+ }
67
105
  if (usedPct === null)
68
106
  throw new Error("grok: weekly percent not found in TUI output");
69
107
  let creditsUsd;
@@ -73,11 +111,13 @@ export function parseGrokTui(text, now = new Date()) {
73
111
  if (Number.isFinite(v) && v >= 0)
74
112
  creditsUsd = v;
75
113
  }
76
- const mReset = cleaned.match(/Resets:\s*([A-Za-z]+\s+\d+,\s*\d+:\d+)/i);
114
+ const resetRe = /Resets:\s*([A-Za-z]+\s+\d+,\s*\d+:\d+)/gi;
115
+ let resetsRaw = null;
116
+ for (const m of cleaned.matchAll(resetRe))
117
+ resetsRaw = m[1].trim();
77
118
  let resetsAt = null;
78
119
  let estimated = false;
79
- if (mReset) {
80
- const resetsRaw = mReset[1].trim();
120
+ if (resetsRaw) {
81
121
  resetsAt = parseGrokReset(resetsRaw, now);
82
122
  if (!resetsAt)
83
123
  throw new Error(`grok: bad resets timestamp "${resetsRaw}"`);
@@ -115,7 +155,10 @@ export const grokAdapter = {
115
155
  rows: 50,
116
156
  settleDelayMs: 5000,
117
157
  input: "/usage\r",
118
- completionRegex: /Weekly lim|Credits:/i,
158
+ // Wait for the usage dialog (tier header or Resets line), not the
159
+ // startup status line "Weekly limit left: N%", which matches /Weekly lim/
160
+ // and would complete before the dialog renders.
161
+ completionRegex: /Weekly\s+lim[i!l]?t\s*\(|Resets:|Credits:/i,
119
162
  timeoutMs: 14000,
120
163
  maxBytes: 256 * 1024,
121
164
  signal: adapterSignal("grok"),
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // generated by scripts/build-embed.mjs — do not edit
2
- export const VERSION = "0.0.27";
2
+ export const VERSION = "0.0.28";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quotacap",
3
- "version": "0.0.27",
3
+ "version": "0.0.28",
4
4
  "description": "Local quota tracker for AI coding plans with a dashboard, CLI, MCP server, and next-plan recommendations.",
5
5
  "license": "MIT",
6
6
  "keywords": [