quotacap 0.0.3 → 0.0.5
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.
- package/dist/adapters/claude.js +2 -52
- package/dist/adapters/manual.js +2 -1
- package/dist/adapters/parse.d.ts +1 -0
- package/dist/adapters/parse.js +81 -0
- package/dist/src/adapters/claude.js +2 -52
- package/dist/src/adapters/manual.js +2 -1
- package/dist/src/adapters/parse.d.ts +1 -0
- package/dist/src/adapters/parse.js +81 -0
- package/dist/src/version.js +1 -1
- package/dist/tests/adapters/manual.test.js +18 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/adapters/claude.js
CHANGED
|
@@ -1,63 +1,13 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import {
|
|
4
|
-
import { fromZonedTime, formatInTimeZone } from "date-fns-tz";
|
|
3
|
+
import { parseResetText } from "./parse.js";
|
|
5
4
|
const exec = promisify(execFile);
|
|
6
|
-
const BRISBANE_TZ = "Australia/Brisbane";
|
|
7
|
-
const RESET_FORMATS = [
|
|
8
|
-
"MMM d 'at' h:mma yyyy",
|
|
9
|
-
"MMM d 'at' ha yyyy",
|
|
10
|
-
"MMM d 'at' h:mm a yyyy",
|
|
11
|
-
"MMM d 'at' h a yyyy",
|
|
12
|
-
];
|
|
13
|
-
function tryParseWithYear(raw, year) {
|
|
14
|
-
const withYear = `${raw.trim()} ${year}`;
|
|
15
|
-
const normalized = withYear.replace(/([ap]m)\b/gi, (m) => m.toUpperCase());
|
|
16
|
-
for (const fmt of RESET_FORMATS) {
|
|
17
|
-
const d = parse(normalized, fmt, new Date());
|
|
18
|
-
if (isValid(d)) {
|
|
19
|
-
return fromZonedTime(d, BRISBANE_TZ);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
function parseBrisbaneReset(resetsRaw, now) {
|
|
25
|
-
const trimmed = resetsRaw.trim();
|
|
26
|
-
if (!trimmed)
|
|
27
|
-
return null;
|
|
28
|
-
const yearStr = formatInTimeZone(now, BRISBANE_TZ, "yyyy");
|
|
29
|
-
const year = parseInt(yearStr, 10);
|
|
30
|
-
let utc = tryParseWithYear(trimmed, year);
|
|
31
|
-
if (!utc)
|
|
32
|
-
return null;
|
|
33
|
-
if (utc.getTime() < now.getTime()) {
|
|
34
|
-
const utcNext = tryParseWithYear(trimmed, year + 1);
|
|
35
|
-
if (utcNext) {
|
|
36
|
-
const diff = utcNext.getTime() - now.getTime();
|
|
37
|
-
if (diff >= 0 && diff < 8 * 86400000) {
|
|
38
|
-
utc = utcNext;
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
// stale / out-of-range (e.g. monthly-plan string) — fallback to now+7d
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
// if utc was past and no valid next-year within window, treat as stale
|
|
49
|
-
if (utc.getTime() < now.getTime())
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
return formatInTimeZone(utc, BRISBANE_TZ, "yyyy-MM-dd'T'HH:mm:ssXXX");
|
|
53
|
-
}
|
|
54
5
|
export function parseClaudeUsage(result, now = new Date()) {
|
|
55
6
|
const sessionMatch = result.match(/Current session:\s+(\d+)% used[^·]*·\s*resets\s+([^\n(]+?)\s*\(/);
|
|
56
7
|
const weeklyMatch = result.match(/Current week \(all models\):\s+(\d+)% used[^·]*·\s*resets\s+([^\n(]+?)\s*\(/);
|
|
57
8
|
const usedPct = weeklyMatch ? parseInt(weeklyMatch[1], 10) : 0;
|
|
58
9
|
const sessionPct = sessionMatch ? parseInt(sessionMatch[1], 10) : undefined;
|
|
59
|
-
|
|
60
|
-
let resetsAt = parseBrisbaneReset(resetsRaw, now);
|
|
10
|
+
let resetsAt = parseResetText(result, now);
|
|
61
11
|
if (!resetsAt)
|
|
62
12
|
resetsAt = new Date(now.getTime() + 7 * 86400000).toISOString();
|
|
63
13
|
return {
|
package/dist/adapters/manual.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { parseResetText } from "./parse.js";
|
|
1
2
|
export function parseManualUsage(provider, text, now = new Date()) {
|
|
2
3
|
const m = text.match(/(\d+)% used/);
|
|
3
4
|
const usedPct = m ? parseInt(m[1], 10) : 0;
|
|
4
|
-
const resetsAt = new Date(now.getTime() + 3 * 86400000).toISOString();
|
|
5
|
+
const resetsAt = parseResetText(text, now) ?? new Date(now.getTime() + 3 * 86400000).toISOString();
|
|
5
6
|
return {
|
|
6
7
|
provider,
|
|
7
8
|
plan: "unknown",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseResetText(text: string, now: Date): string | null;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { parse, isValid } from "date-fns";
|
|
2
|
+
import { fromZonedTime, formatInTimeZone } from "date-fns-tz";
|
|
3
|
+
// Shared reset-date parsing for adapter raw text. Claude and manual pastes
|
|
4
|
+
// both carry "resets Aug 29 at 11am" style strings; this resolves them to an
|
|
5
|
+
// ISO instant in Brisbane time with a same-format next-year rollover.
|
|
6
|
+
const BRISBANE_TZ = "Australia/Brisbane";
|
|
7
|
+
const RESET_FORMATS = [
|
|
8
|
+
"MMM d 'at' h:mma yyyy",
|
|
9
|
+
"MMM d 'at' ha yyyy",
|
|
10
|
+
"MMM d 'at' h:mm a yyyy",
|
|
11
|
+
"MMM d 'at' h a yyyy",
|
|
12
|
+
];
|
|
13
|
+
function tryParseWithYear(raw, year) {
|
|
14
|
+
const withYear = `${raw.trim()} ${year}`;
|
|
15
|
+
const normalized = withYear.replace(/([ap]m)\b/gi, (m) => m.toUpperCase());
|
|
16
|
+
for (const fmt of RESET_FORMATS) {
|
|
17
|
+
const d = parse(normalized, fmt, new Date());
|
|
18
|
+
if (isValid(d)) {
|
|
19
|
+
return fromZonedTime(d, BRISBANE_TZ);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function parseAbsoluteReset(raw, now) {
|
|
25
|
+
const yearStr = formatInTimeZone(now, BRISBANE_TZ, "yyyy");
|
|
26
|
+
const year = parseInt(yearStr, 10);
|
|
27
|
+
let utc = tryParseWithYear(raw, year);
|
|
28
|
+
if (!utc)
|
|
29
|
+
return null;
|
|
30
|
+
if (utc.getTime() < now.getTime()) {
|
|
31
|
+
const utcNext = tryParseWithYear(raw, year + 1);
|
|
32
|
+
if (utcNext) {
|
|
33
|
+
const diff = utcNext.getTime() - now.getTime();
|
|
34
|
+
if (diff >= 0 && diff < 8 * 86400000) {
|
|
35
|
+
utc = utcNext;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (utc.getTime() < now.getTime())
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return utc;
|
|
48
|
+
}
|
|
49
|
+
function parseRelativeDuration(raw, now) {
|
|
50
|
+
const m = raw.match(/^in\s+(.+)$/);
|
|
51
|
+
if (!m)
|
|
52
|
+
return null;
|
|
53
|
+
const inner = m[1].trim();
|
|
54
|
+
const d = inner.match(/(\d+)d/)?.[1];
|
|
55
|
+
const h = inner.match(/(\d+)h/)?.[1];
|
|
56
|
+
const min = inner.match(/(\d+)m/)?.[1];
|
|
57
|
+
if (!d && !h && !min)
|
|
58
|
+
return null;
|
|
59
|
+
const ms = (parseInt(d ?? "0", 10) * 86400 + parseInt(h ?? "0", 10) * 3600 + parseInt(min ?? "0", 10) * 60) * 1000;
|
|
60
|
+
return new Date(now.getTime() + ms);
|
|
61
|
+
}
|
|
62
|
+
export function parseResetText(text, now) {
|
|
63
|
+
// Farthest future reset wins. Claude output lists the session reset before
|
|
64
|
+
// the weekly one; kimi lists the weekly window before its 5h rolling one —
|
|
65
|
+
// the period reset is always the farthest, so one rule serves both.
|
|
66
|
+
const matches = [...text.matchAll(/resets\s+([^\n(]+?)\s*(?:\(|$)/gm)];
|
|
67
|
+
let best = null;
|
|
68
|
+
for (const m of matches) {
|
|
69
|
+
const raw = m[1].trim();
|
|
70
|
+
if (!raw)
|
|
71
|
+
continue;
|
|
72
|
+
const candidates = [parseAbsoluteReset(raw, now), parseRelativeDuration(raw, now)];
|
|
73
|
+
for (const c of candidates) {
|
|
74
|
+
if (c && (!best || c.getTime() > best.getTime()))
|
|
75
|
+
best = c;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!best)
|
|
79
|
+
return null;
|
|
80
|
+
return formatInTimeZone(best, BRISBANE_TZ, "yyyy-MM-dd'T'HH:mm:ssXXX");
|
|
81
|
+
}
|
|
@@ -1,63 +1,13 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import {
|
|
4
|
-
import { fromZonedTime, formatInTimeZone } from "date-fns-tz";
|
|
3
|
+
import { parseResetText } from "./parse.js";
|
|
5
4
|
const exec = promisify(execFile);
|
|
6
|
-
const BRISBANE_TZ = "Australia/Brisbane";
|
|
7
|
-
const RESET_FORMATS = [
|
|
8
|
-
"MMM d 'at' h:mma yyyy",
|
|
9
|
-
"MMM d 'at' ha yyyy",
|
|
10
|
-
"MMM d 'at' h:mm a yyyy",
|
|
11
|
-
"MMM d 'at' h a yyyy",
|
|
12
|
-
];
|
|
13
|
-
function tryParseWithYear(raw, year) {
|
|
14
|
-
const withYear = `${raw.trim()} ${year}`;
|
|
15
|
-
const normalized = withYear.replace(/([ap]m)\b/gi, (m) => m.toUpperCase());
|
|
16
|
-
for (const fmt of RESET_FORMATS) {
|
|
17
|
-
const d = parse(normalized, fmt, new Date());
|
|
18
|
-
if (isValid(d)) {
|
|
19
|
-
return fromZonedTime(d, BRISBANE_TZ);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
function parseBrisbaneReset(resetsRaw, now) {
|
|
25
|
-
const trimmed = resetsRaw.trim();
|
|
26
|
-
if (!trimmed)
|
|
27
|
-
return null;
|
|
28
|
-
const yearStr = formatInTimeZone(now, BRISBANE_TZ, "yyyy");
|
|
29
|
-
const year = parseInt(yearStr, 10);
|
|
30
|
-
let utc = tryParseWithYear(trimmed, year);
|
|
31
|
-
if (!utc)
|
|
32
|
-
return null;
|
|
33
|
-
if (utc.getTime() < now.getTime()) {
|
|
34
|
-
const utcNext = tryParseWithYear(trimmed, year + 1);
|
|
35
|
-
if (utcNext) {
|
|
36
|
-
const diff = utcNext.getTime() - now.getTime();
|
|
37
|
-
if (diff >= 0 && diff < 8 * 86400000) {
|
|
38
|
-
utc = utcNext;
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
// stale / out-of-range (e.g. monthly-plan string) — fallback to now+7d
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
// if utc was past and no valid next-year within window, treat as stale
|
|
49
|
-
if (utc.getTime() < now.getTime())
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
return formatInTimeZone(utc, BRISBANE_TZ, "yyyy-MM-dd'T'HH:mm:ssXXX");
|
|
53
|
-
}
|
|
54
5
|
export function parseClaudeUsage(result, now = new Date()) {
|
|
55
6
|
const sessionMatch = result.match(/Current session:\s+(\d+)% used[^·]*·\s*resets\s+([^\n(]+?)\s*\(/);
|
|
56
7
|
const weeklyMatch = result.match(/Current week \(all models\):\s+(\d+)% used[^·]*·\s*resets\s+([^\n(]+?)\s*\(/);
|
|
57
8
|
const usedPct = weeklyMatch ? parseInt(weeklyMatch[1], 10) : 0;
|
|
58
9
|
const sessionPct = sessionMatch ? parseInt(sessionMatch[1], 10) : undefined;
|
|
59
|
-
|
|
60
|
-
let resetsAt = parseBrisbaneReset(resetsRaw, now);
|
|
10
|
+
let resetsAt = parseResetText(result, now);
|
|
61
11
|
if (!resetsAt)
|
|
62
12
|
resetsAt = new Date(now.getTime() + 7 * 86400000).toISOString();
|
|
63
13
|
return {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { parseResetText } from "./parse.js";
|
|
1
2
|
export function parseManualUsage(provider, text, now = new Date()) {
|
|
2
3
|
const m = text.match(/(\d+)% used/);
|
|
3
4
|
const usedPct = m ? parseInt(m[1], 10) : 0;
|
|
4
|
-
const resetsAt = new Date(now.getTime() + 3 * 86400000).toISOString();
|
|
5
|
+
const resetsAt = parseResetText(text, now) ?? new Date(now.getTime() + 3 * 86400000).toISOString();
|
|
5
6
|
return {
|
|
6
7
|
provider,
|
|
7
8
|
plan: "unknown",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseResetText(text: string, now: Date): string | null;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { parse, isValid } from "date-fns";
|
|
2
|
+
import { fromZonedTime, formatInTimeZone } from "date-fns-tz";
|
|
3
|
+
// Shared reset-date parsing for adapter raw text. Claude and manual pastes
|
|
4
|
+
// both carry "resets Aug 29 at 11am" style strings; this resolves them to an
|
|
5
|
+
// ISO instant in Brisbane time with a same-format next-year rollover.
|
|
6
|
+
const BRISBANE_TZ = "Australia/Brisbane";
|
|
7
|
+
const RESET_FORMATS = [
|
|
8
|
+
"MMM d 'at' h:mma yyyy",
|
|
9
|
+
"MMM d 'at' ha yyyy",
|
|
10
|
+
"MMM d 'at' h:mm a yyyy",
|
|
11
|
+
"MMM d 'at' h a yyyy",
|
|
12
|
+
];
|
|
13
|
+
function tryParseWithYear(raw, year) {
|
|
14
|
+
const withYear = `${raw.trim()} ${year}`;
|
|
15
|
+
const normalized = withYear.replace(/([ap]m)\b/gi, (m) => m.toUpperCase());
|
|
16
|
+
for (const fmt of RESET_FORMATS) {
|
|
17
|
+
const d = parse(normalized, fmt, new Date());
|
|
18
|
+
if (isValid(d)) {
|
|
19
|
+
return fromZonedTime(d, BRISBANE_TZ);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function parseAbsoluteReset(raw, now) {
|
|
25
|
+
const yearStr = formatInTimeZone(now, BRISBANE_TZ, "yyyy");
|
|
26
|
+
const year = parseInt(yearStr, 10);
|
|
27
|
+
let utc = tryParseWithYear(raw, year);
|
|
28
|
+
if (!utc)
|
|
29
|
+
return null;
|
|
30
|
+
if (utc.getTime() < now.getTime()) {
|
|
31
|
+
const utcNext = tryParseWithYear(raw, year + 1);
|
|
32
|
+
if (utcNext) {
|
|
33
|
+
const diff = utcNext.getTime() - now.getTime();
|
|
34
|
+
if (diff >= 0 && diff < 8 * 86400000) {
|
|
35
|
+
utc = utcNext;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (utc.getTime() < now.getTime())
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return utc;
|
|
48
|
+
}
|
|
49
|
+
function parseRelativeDuration(raw, now) {
|
|
50
|
+
const m = raw.match(/^in\s+(.+)$/);
|
|
51
|
+
if (!m)
|
|
52
|
+
return null;
|
|
53
|
+
const inner = m[1].trim();
|
|
54
|
+
const d = inner.match(/(\d+)d/)?.[1];
|
|
55
|
+
const h = inner.match(/(\d+)h/)?.[1];
|
|
56
|
+
const min = inner.match(/(\d+)m/)?.[1];
|
|
57
|
+
if (!d && !h && !min)
|
|
58
|
+
return null;
|
|
59
|
+
const ms = (parseInt(d ?? "0", 10) * 86400 + parseInt(h ?? "0", 10) * 3600 + parseInt(min ?? "0", 10) * 60) * 1000;
|
|
60
|
+
return new Date(now.getTime() + ms);
|
|
61
|
+
}
|
|
62
|
+
export function parseResetText(text, now) {
|
|
63
|
+
// Farthest future reset wins. Claude output lists the session reset before
|
|
64
|
+
// the weekly one; kimi lists the weekly window before its 5h rolling one —
|
|
65
|
+
// the period reset is always the farthest, so one rule serves both.
|
|
66
|
+
const matches = [...text.matchAll(/resets\s+([^\n(]+?)\s*(?:\(|$)/gm)];
|
|
67
|
+
let best = null;
|
|
68
|
+
for (const m of matches) {
|
|
69
|
+
const raw = m[1].trim();
|
|
70
|
+
if (!raw)
|
|
71
|
+
continue;
|
|
72
|
+
const candidates = [parseAbsoluteReset(raw, now), parseRelativeDuration(raw, now)];
|
|
73
|
+
for (const c of candidates) {
|
|
74
|
+
if (c && (!best || c.getTime() > best.getTime()))
|
|
75
|
+
best = c;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!best)
|
|
79
|
+
return null;
|
|
80
|
+
return formatInTimeZone(best, BRISBANE_TZ, "yyyy-MM-dd'T'HH:mm:ssXXX");
|
|
81
|
+
}
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// generated by scripts/build-embed.mjs — do not edit
|
|
2
|
-
export const VERSION = "0.0.
|
|
2
|
+
export const VERSION = "0.0.5";
|
|
@@ -7,4 +7,22 @@ describe("manual", () => {
|
|
|
7
7
|
expect(q.usedPct).toBe(22);
|
|
8
8
|
expect(q.source).toBe("manual");
|
|
9
9
|
});
|
|
10
|
+
it("parses the reset date from the text instead of guessing +3d", () => {
|
|
11
|
+
const now = new Date("2026-08-28T06:00:00+10:00");
|
|
12
|
+
const q = parseManualUsage("kimi", `Current week: 22% used · resets Aug 29 at 11am`, now);
|
|
13
|
+
expect(q.resetsAt).toMatch(/^2026-08-29/);
|
|
14
|
+
});
|
|
15
|
+
it("parses kimi's real dashboard format (relative duration, weekly window)", () => {
|
|
16
|
+
const now = new Date("2026-08-28T22:00:00Z");
|
|
17
|
+
const text = [
|
|
18
|
+
"Usage",
|
|
19
|
+
"Session usage: No active session.",
|
|
20
|
+
"Weekly limit ███░░░░░░░░░░░░░░░░░ 16% used resets in 3d 1h 24m",
|
|
21
|
+
"5h limit ░░░░░░░░░░░░░░░░░░░░ 0% used resets in 3h 24m",
|
|
22
|
+
].join("\n");
|
|
23
|
+
const q = parseManualUsage("kimi", text, now);
|
|
24
|
+
expect(q.usedPct).toBe(16);
|
|
25
|
+
const expected = now.getTime() + ((3 * 24 + 1) * 3600 + 24 * 60) * 1000;
|
|
26
|
+
expect(new Date(q.resetsAt).getTime()).toBe(expected);
|
|
27
|
+
});
|
|
10
28
|
});
|
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.
|
|
2
|
+
export const VERSION = "0.0.5";
|