quotacap 0.0.8 → 0.0.9

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,2 +1,2 @@
1
1
  // generated by scripts/build-embed.mjs — do not edit
2
- export const webHtml = "<!doctype html><html><head><meta charset=\"utf-8\"><title>QuotaCap</title> <script type=\"module\" crossorigin src=\"/assets/index-M-4j9c6v.js\"></script>\n</head><body><div id=\"app\"></div></body></html>\n";
2
+ export const webHtml = "<!doctype html><html><head><meta charset=\"utf-8\"><title>QuotaCap</title> <script type=\"module\" crossorigin src=\"/assets/index-C7g0JA14.js\"></script>\n</head><body><div id=\"app\"></div></body></html>\n";
@@ -1,10 +1,26 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { execFile } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
4
7
  const exec = promisify(execFile);
5
8
  describe("cli", () => {
6
9
  it("--help lists commands", async () => {
7
10
  const { stdout } = await exec("node", ["dist/cli/index.js", "--help"]);
8
11
  expect(stdout).toMatch(/status/);
9
12
  });
13
+ it("status prints the shared quota table", async () => {
14
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), "qc-cli-"));
15
+ const env = { ...process.env, HOME: home };
16
+ try {
17
+ await exec("node", ["dist/cli/index.js", "ingest", "--provider", "kimi", "--text", "Weekly limit 16% used resets in 3d"], { env });
18
+ const { stdout } = await exec("node", ["dist/cli/index.js", "status"], { env });
19
+ expect(stdout).toMatch(/\| Provider \| Used \| Remaining \| Resets \| Days left \|/);
20
+ expect(stdout).toMatch(/\| kimi \| 16% \| 84% \|/);
21
+ }
22
+ finally {
23
+ fs.rmSync(home, { recursive: true, force: true });
24
+ }
25
+ }, 10000);
10
26
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { renderQuotasTable } from "../../src/format/table.js";
3
+ describe("format table", () => {
4
+ it("renders a markdown table with one row per provider", () => {
5
+ const quotas = [
6
+ { provider: "kimi", usedPct: 16, resetsAt: "2026-09-01T09:02:00+10:00" },
7
+ { provider: "claude", usedPct: 37, resetsAt: "2026-09-03T21:00:00+10:00" },
8
+ ];
9
+ const advisories = [
10
+ { provider: "kimi", wastePct: 78.0, daysLeft: 3.0, idealRate: 28.0, burnRate: 40, burnMeasured: true, status: "at risk" },
11
+ { provider: "claude", wastePct: 52.9, daysLeft: 5.5, idealRate: 11.6, burnRate: 2, burnMeasured: false, status: "on track" },
12
+ ];
13
+ const table = renderQuotasTable(quotas, advisories);
14
+ expect(table).toMatch(/\| Provider \| Used \| Remaining \| Resets \| Days left \| Ideal daily burn \| Burn rate \| Status \| Waste if unused \|/);
15
+ expect(table).toMatch(/\| kimi \| 16% \| 84% \|.*\| 3.0 \| 28%\/day \| 40.0%\/day \| ⚠️ \| 78% \|/);
16
+ expect(table).toMatch(/\| claude \| 37% \| 63% \|.*\| 5.5 \| 12%\/day \| collecting… \| ✅ \| 53% \|/);
17
+ });
18
+ it("renders quota-only rows with placeholders when no advisories exist", () => {
19
+ const quotas = [{ provider: "kimi", usedPct: 16, resetsAt: "2026-09-01T09:02:00+10:00" }];
20
+ const table = renderQuotasTable(quotas, []);
21
+ expect(table).toMatch(/\| kimi \| 16% \| 84% \|.*\| — \| — \| — \| — \| — \|/);
22
+ });
23
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { handleTool } from "../../src/mcp/server.js";
3
+ import { buildApp } from "../../src/http/server.js";
4
+ import { openDb, migrate } from "../../src/store/db.js";
5
+ import { upsertQuota } from "../../src/store/quotas.js";
6
+ describe("mcp get_quotas", () => {
7
+ it("renders the shared table for quota status", async () => {
8
+ const db = openDb(":memory:");
9
+ migrate(db);
10
+ upsertQuota(db, { provider: "claude", plan: "max", usedPct: 40, resetsAt: "2026-09-03T21:00:00+10:00", periodStart: "2026-08-26T00:00:00Z", raw: "x", source: "cli", fetchedAt: new Date().toISOString() });
11
+ const app = buildApp(db);
12
+ const addr = await app.listen({ port: 0, host: "127.0.0.1" });
13
+ process.env.QUOTACAP_URL = addr;
14
+ try {
15
+ const res = await handleTool("get_quotas", {});
16
+ expect(res.content[0].text).toMatch(/\| Provider \| Used \| Remaining \| Resets \| Days left \|/);
17
+ expect(res.content[0].text).toMatch(/\| claude \| 40% \| 60% \|/);
18
+ }
19
+ finally {
20
+ delete process.env.QUOTACAP_URL;
21
+ await app.close();
22
+ }
23
+ });
24
+ });
@@ -1,25 +1,7 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { tools, recommendationTable } from "../../src/mcp/server.js";
2
+ import { tools } from "../../src/mcp/server.js";
3
3
  describe("mcp", () => {
4
4
  it("exposes three tools", () => {
5
5
  expect(tools.map(t => t.name)).toEqual(expect.arrayContaining(["get_quotas", "get_recommendation", "forecast"]));
6
6
  });
7
- it("renders a markdown table with one row per provider", () => {
8
- const rec = {
9
- use: "kimi",
10
- reason: "78% waste in 3.0d",
11
- alternatives: [
12
- { provider: "kimi", usedPct: 16, resetsAt: "2026-09-01T09:02:00+10:00" },
13
- { provider: "claude", usedPct: 37, resetsAt: "2026-09-03T21:00:00+10:00" },
14
- ],
15
- advisories: [
16
- { provider: "kimi", wastePct: 78.0, daysLeft: 3.0, idealRate: 28.0, burnRate: 40, burnMeasured: true, status: "at risk" },
17
- { provider: "claude", wastePct: 52.9, daysLeft: 5.5, idealRate: 11.6, burnRate: 2, burnMeasured: false, status: "on track" },
18
- ],
19
- };
20
- const table = recommendationTable(rec);
21
- expect(table).toMatch(/\| Provider \| Used \| Remaining \| Resets \| Days left \| Ideal daily burn \| Burn rate \| Status \| Waste if unused \|/);
22
- expect(table).toMatch(/\| kimi \| 16% \| 84% \|.*\| 3.0 \| 28%\/day \| 40.0%\/day \| ⚠️ \| 78% \|/);
23
- expect(table).toMatch(/\| claude \| 37% \| 63% \|.*\| 5.5 \| 12%\/day \| collecting… \| ✅ \| 53% \|/);
24
- });
25
7
  });
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.8";
2
+ export const VERSION = "0.0.9";
@@ -42,7 +42,7 @@ function App() {
42
42
  advisoriesByProvider.set(a.provider, a);
43
43
  const hasStale = sorted.some((q) => q.stale ?? (q.fetchedAt ? Date.now() - new Date(q.fetchedAt).getTime() > 60 * 60 * 1000 : false));
44
44
  const isDegraded = !!fetchError || (quotas.length === 0 && !rec);
45
- return (_jsxs("div", { style: { fontFamily: "system-ui", padding: 16, maxWidth: 900, margin: "0 auto" }, children: [_jsx("h1", { style: { fontSize: 20, margin: "0 0 12px" }, children: "QuotaCap" }), _jsx(Banner, { rec: rec }), isDegraded && (_jsxs("div", { "data-testid": "error-banner", style: { background: "#fecaca", padding: 10, borderRadius: 8, marginBottom: 12, fontSize: 13 }, children: ["no fresh data", lastPollAt ? `, last ${new Date(lastPollAt).toLocaleString()}` : "", " \u2014 check network/daemon", fetchError ? `: ${fetchError}` : ""] })), hasStale && (_jsxs("div", { "data-testid": "stale-banner", style: { background: "#ffedd5", padding: 8, borderRadius: 8, marginBottom: 12, fontSize: 12 }, children: ["some quotas stale \u2014 ", _jsx("button", { onClick: () => fetch("/api/refresh", { method: "POST" }).then(() => location.reload()), style: { border: "1px solid #d1d5db", background: "#fff", borderRadius: 4, padding: "2px 6px", cursor: "pointer" }, children: "Refresh now" })] })), _jsxs("div", { "data-testid": "strip", style: { display: "flex", gap: 8, margin: "12px 0", overflowX: "auto", alignItems: "center" }, children: [_jsx("span", { style: { color: "#2563eb", fontWeight: 700 }, children: "NOW" }), sorted.map((q) => (_jsxs("span", { style: { border: "1px solid #d1d5db", borderRadius: 999, padding: "4px 10px", whiteSpace: "nowrap", background: "#fff" }, children: [q.provider, " ", formatReset(q.resetsAt)] }, q.provider))), sorted.length === 0 && _jsx("span", { style: { color: "#9ca3af" }, children: "no quotas yet" })] }), _jsxs("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 14 }, children: [_jsx("thead", { children: _jsxs("tr", { style: { textAlign: "left", borderBottom: "2px solid #e5e7eb" }, children: [_jsx("th", { style: { padding: "8px 6px" }, children: "Provider" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Quota" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Rate" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Ideal" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Advice" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Status" }), _jsx("th", { style: { padding: "8px 6px" } })] }) }), _jsxs("tbody", { children: [sorted.map((q) => {
45
+ return (_jsxs("div", { style: { fontFamily: "system-ui", padding: 16, maxWidth: 900, margin: "0 auto" }, children: [_jsx("h1", { style: { fontSize: 20, margin: "0 0 12px" }, children: "QuotaCap" }), _jsx(Banner, { rec: rec }), isDegraded && (_jsxs("div", { "data-testid": "error-banner", style: { background: "#fecaca", padding: 10, borderRadius: 8, marginBottom: 12, fontSize: 13 }, children: ["no fresh data", lastPollAt ? `, last ${new Date(lastPollAt).toLocaleString()}` : "", " \u2014 check network/daemon", fetchError ? `: ${fetchError}` : ""] })), hasStale && (_jsxs("div", { "data-testid": "stale-banner", style: { background: "#ffedd5", padding: 8, borderRadius: 8, marginBottom: 12, fontSize: 12 }, children: ["some quotas stale \u2014 ", _jsx("button", { onClick: () => fetch("/api/refresh", { method: "POST" }).then(() => location.reload()), style: { border: "1px solid #d1d5db", background: "#fff", borderRadius: 4, padding: "2px 6px", cursor: "pointer" }, children: "Refresh now" })] })), _jsxs("div", { "data-testid": "strip", style: { display: "flex", gap: 8, margin: "12px 0", overflowX: "auto", alignItems: "center" }, children: [_jsx("span", { style: { color: "#2563eb", fontWeight: 700 }, children: "NOW" }), sorted.map((q) => (_jsxs("span", { style: { border: "1px solid #d1d5db", borderRadius: 999, padding: "4px 10px", whiteSpace: "nowrap", background: "#fff" }, children: [q.provider, " ", formatReset(q.resetsAt)] }, q.provider))), sorted.length === 0 && _jsx("span", { style: { color: "#9ca3af" }, children: "no quotas yet" })] }), _jsxs("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 14 }, children: [_jsx("thead", { children: _jsxs("tr", { style: { textAlign: "left", borderBottom: "2px solid #e5e7eb" }, children: [_jsx("th", { style: { padding: "8px 6px" }, children: "Provider" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Quota" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Remaining" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Resets" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Days left" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Ideal" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Rate" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Status" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Waste" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Advice" }), _jsx("th", { style: { padding: "8px 6px" } })] }) }), _jsxs("tbody", { children: [sorted.map((q) => {
46
46
  const adv = advisoriesByProvider.get(q.provider);
47
47
  const waste = adv?.wastePct ?? 0;
48
48
  const ideal = adv?.idealRate;
@@ -56,7 +56,7 @@ function App() {
56
56
  width: `${Math.min(100, q.usedPct)}%`,
57
57
  height: "100%",
58
58
  background: waste > 30 ? "#f59e0b" : "#3b82f6",
59
- } }) }), _jsxs("span", { children: [q.usedPct, "%"] })] }) }), _jsx("td", { style: { padding: "8px 6px" }, children: burn != null ? (burnMeasured ? `${burn.toFixed(1)}%/d` : "collecting…") : "—" }), _jsx("td", { style: { padding: "8px 6px" }, children: ideal != null ? `${ideal.toFixed(1)}%/d` : "—" }), _jsx("td", { style: { padding: "8px 6px" }, children: urgency || "—" }), _jsx("td", { style: { padding: "8px 6px" }, children: status === "at risk" ? "⚠️ at risk" : "✅ on track" }), _jsx("td", { style: { padding: "8px 6px" }, children: _jsx("button", { "aria-label": `expand ${q.provider}`, onClick: () => setExpanded(isExpanded ? null : q.provider), style: { cursor: "pointer", border: "1px solid #d1d5db", background: "#fff", borderRadius: 4, padding: "2px 6px" }, children: isExpanded ? "▼" : "▶" }) })] }), isExpanded && (_jsx("tr", { children: _jsx("td", { colSpan: 6, style: { padding: 0 }, children: _jsxs("div", { style: { padding: 12, background: "#f9fafb", borderTop: "1px solid #e5e7eb" }, children: [_jsxs("div", { style: { position: "relative", height: 24, background: "#e5e7eb", borderRadius: 4, overflow: "hidden", marginBottom: 8 }, children: [_jsx("div", { style: {
59
+ } }) }), _jsxs("span", { children: [q.usedPct, "%"] })] }) }), _jsxs("td", { style: { padding: "8px 6px" }, children: [100 - q.usedPct, "%"] }), _jsx("td", { style: { padding: "8px 6px" }, children: formatReset(q.resetsAt) }), _jsx("td", { style: { padding: "8px 6px" }, children: adv?.daysLeft != null ? adv.daysLeft.toFixed(1) : "—" }), _jsx("td", { style: { padding: "8px 6px" }, children: ideal != null ? `${ideal.toFixed(1)}%/d` : "—" }), _jsx("td", { style: { padding: "8px 6px" }, children: burn != null ? (burnMeasured ? `${burn.toFixed(1)}%/d` : "collecting…") : "—" }), _jsx("td", { style: { padding: "8px 6px" }, children: status === "at risk" ? "⚠️ at risk" : "✅ on track" }), _jsx("td", { style: { padding: "8px 6px" }, children: adv?.wastePct != null ? `${Math.round(adv.wastePct)}%` : "—" }), _jsx("td", { style: { padding: "8px 6px" }, children: urgency || "—" }), _jsx("td", { style: { padding: "8px 6px" }, children: _jsx("button", { "aria-label": `expand ${q.provider}`, onClick: () => setExpanded(isExpanded ? null : q.provider), style: { cursor: "pointer", border: "1px solid #d1d5db", background: "#fff", borderRadius: 4, padding: "2px 6px" }, children: isExpanded ? "▼" : "▶" }) })] }), isExpanded && (_jsx("tr", { children: _jsx("td", { colSpan: 6, style: { padding: 0 }, children: _jsxs("div", { style: { padding: 12, background: "#f9fafb", borderTop: "1px solid #e5e7eb" }, children: [_jsxs("div", { style: { position: "relative", height: 24, background: "#e5e7eb", borderRadius: 4, overflow: "hidden", marginBottom: 8 }, children: [_jsx("div", { style: {
60
60
  position: "absolute",
61
61
  left: "10%",
62
62
  top: 0,