quotacap 0.0.2 → 0.0.4

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.
@@ -1,63 +1,13 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { parse, isValid } from "date-fns";
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
- const resetsRaw = weeklyMatch?.[2].trim() ?? "";
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,56 @@
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
+ export function parseResetText(text, now) {
25
+ // Last "resets" match wins: claude output lists the session reset first and
26
+ // the weekly (period) reset last; manual pastes carry a single line.
27
+ const matches = [...text.matchAll(/resets\s+([^\n(]+?)\s*(?:\(|$)/gm)];
28
+ if (!matches.length)
29
+ return null;
30
+ const trimmed = matches[matches.length - 1][1].trim();
31
+ if (!trimmed)
32
+ return null;
33
+ const yearStr = formatInTimeZone(now, BRISBANE_TZ, "yyyy");
34
+ const year = parseInt(yearStr, 10);
35
+ let utc = tryParseWithYear(trimmed, year);
36
+ if (!utc)
37
+ return null;
38
+ if (utc.getTime() < now.getTime()) {
39
+ const utcNext = tryParseWithYear(trimmed, year + 1);
40
+ if (utcNext) {
41
+ const diff = utcNext.getTime() - now.getTime();
42
+ if (diff >= 0 && diff < 8 * 86400000) {
43
+ utc = utcNext;
44
+ }
45
+ else {
46
+ return null;
47
+ }
48
+ }
49
+ else {
50
+ return null;
51
+ }
52
+ if (utc.getTime() < now.getTime())
53
+ return null;
54
+ }
55
+ return formatInTimeZone(utc, BRISBANE_TZ, "yyyy-MM-dd'T'HH:mm:ssXXX");
56
+ }
package/dist/cli/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { Command } from "commander";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
+ import { VERSION } from "../version.js";
5
6
  import { buildApp } from "../http/server.js";
6
7
  import { openDb, migrate } from "../store/db.js";
7
8
  import { getDbPath, readConfig } from "../config.js";
@@ -10,7 +11,8 @@ function ensureDbDir() { try {
10
11
  }
11
12
  catch { } }
12
13
  const program = new Command();
13
- program.name("quotacap").version("0.0.2");
14
+ program.name("quotacap").version(VERSION);
15
+ program.command("version").action(() => console.log(VERSION));
14
16
  program.command("status").option("--json", "json").action(async (opts) => {
15
17
  ensureDbDir();
16
18
  const db = openDb(getDbPath());
@@ -1,63 +1,13 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { parse, isValid } from "date-fns";
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
- const resetsRaw = weeklyMatch?.[2].trim() ?? "";
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,56 @@
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
+ export function parseResetText(text, now) {
25
+ // Last "resets" match wins: claude output lists the session reset first and
26
+ // the weekly (period) reset last; manual pastes carry a single line.
27
+ const matches = [...text.matchAll(/resets\s+([^\n(]+?)\s*(?:\(|$)/gm)];
28
+ if (!matches.length)
29
+ return null;
30
+ const trimmed = matches[matches.length - 1][1].trim();
31
+ if (!trimmed)
32
+ return null;
33
+ const yearStr = formatInTimeZone(now, BRISBANE_TZ, "yyyy");
34
+ const year = parseInt(yearStr, 10);
35
+ let utc = tryParseWithYear(trimmed, year);
36
+ if (!utc)
37
+ return null;
38
+ if (utc.getTime() < now.getTime()) {
39
+ const utcNext = tryParseWithYear(trimmed, year + 1);
40
+ if (utcNext) {
41
+ const diff = utcNext.getTime() - now.getTime();
42
+ if (diff >= 0 && diff < 8 * 86400000) {
43
+ utc = utcNext;
44
+ }
45
+ else {
46
+ return null;
47
+ }
48
+ }
49
+ else {
50
+ return null;
51
+ }
52
+ if (utc.getTime() < now.getTime())
53
+ return null;
54
+ }
55
+ return formatInTimeZone(utc, BRISBANE_TZ, "yyyy-MM-dd'T'HH:mm:ssXXX");
56
+ }
@@ -2,6 +2,7 @@
2
2
  import { Command } from "commander";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
+ import { VERSION } from "../version.js";
5
6
  import { buildApp } from "../http/server.js";
6
7
  import { openDb, migrate } from "../store/db.js";
7
8
  import { getDbPath, readConfig } from "../config.js";
@@ -10,7 +11,8 @@ function ensureDbDir() { try {
10
11
  }
11
12
  catch { } }
12
13
  const program = new Command();
13
- program.name("quotacap").version("0.0.2");
14
+ program.name("quotacap").version(VERSION);
15
+ program.command("version").action(() => console.log(VERSION));
14
16
  program.command("status").option("--json", "json").action(async (opts) => {
15
17
  ensureDbDir();
16
18
  const db = openDb(getDbPath());
@@ -0,0 +1 @@
1
+ export declare const VERSION: string;
@@ -0,0 +1,2 @@
1
+ // generated by scripts/build-embed.mjs — do not edit
2
+ export const VERSION = "0.0.4";
@@ -7,4 +7,9 @@ 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
+ });
10
15
  });
@@ -0,0 +1 @@
1
+ export declare const VERSION: string;
@@ -0,0 +1,2 @@
1
+ // generated by scripts/build-embed.mjs — do not edit
2
+ export const VERSION = "0.0.4";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quotacap",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",