quotacap 0.0.7 → 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.
Files changed (40) hide show
  1. package/dist/advisory/engine.d.ts +1 -1
  2. package/dist/advisory/engine.js +6 -3
  3. package/dist/advisory/types.d.ts +4 -0
  4. package/dist/cli/index.js +7 -3
  5. package/dist/format/table.d.ts +1 -0
  6. package/dist/format/table.js +21 -0
  7. package/dist/http/server.js +3 -1
  8. package/dist/mcp/server.d.ts +12 -2
  9. package/dist/mcp/server.js +8 -18
  10. package/dist/src/advisory/engine.d.ts +1 -1
  11. package/dist/src/advisory/engine.js +6 -3
  12. package/dist/src/advisory/types.d.ts +4 -0
  13. package/dist/src/cli/index.js +7 -3
  14. package/dist/src/format/table.d.ts +1 -0
  15. package/dist/src/format/table.js +21 -0
  16. package/dist/src/http/server.js +3 -1
  17. package/dist/src/mcp/server.d.ts +12 -2
  18. package/dist/src/mcp/server.js +8 -18
  19. package/dist/src/store/quotas.d.ts +1 -0
  20. package/dist/src/store/quotas.js +24 -0
  21. package/dist/src/version.js +1 -1
  22. package/dist/src/webAssets.js +1 -1
  23. package/dist/src/webHtml.js +1 -1
  24. package/dist/store/quotas.d.ts +1 -0
  25. package/dist/store/quotas.js +24 -0
  26. package/dist/tests/advisory/engine.test.js +23 -0
  27. package/dist/tests/cli/cli.test.js +16 -0
  28. package/dist/tests/format/table.test.d.ts +1 -0
  29. package/dist/tests/format/table.test.js +23 -0
  30. package/dist/tests/mcp/quotas.test.d.ts +1 -0
  31. package/dist/tests/mcp/quotas.test.js +24 -0
  32. package/dist/tests/mcp/server.test.js +1 -19
  33. package/dist/tests/store/db.test.js +18 -1
  34. package/dist/version.js +1 -1
  35. package/dist/web/src/App.js +4 -2
  36. package/dist/webAssets.js +1 -1
  37. package/dist/webHtml.js +1 -1
  38. package/package.json +1 -1
  39. package/web/dist/assets/{index-G2N84rBQ.js → index-C7g0JA14.js} +8 -8
  40. package/web/dist/index.html +1 -1
@@ -1,6 +1,6 @@
1
1
  import type { Quota } from "../adapters/types.js";
2
2
  import type { Advisory } from "./types.js";
3
- export declare function computeAdvisory(q: Quota, burnRate: number, now?: Date): Advisory;
3
+ export declare function computeAdvisory(q: Quota, burnRate: number, now?: Date, burnMeasured?: boolean): Advisory;
4
4
  export declare function recommend(quotas: Quota[], _task: string, burnByProvider?: Map<string, number>, now?: Date): {
5
5
  use: string;
6
6
  reason: string;
@@ -1,9 +1,12 @@
1
- export function computeAdvisory(q, burnRate, now = new Date()) {
1
+ export function computeAdvisory(q, burnRate, now = new Date(), burnMeasured = false) {
2
2
  const resets = new Date(q.resetsAt);
3
3
  const daysLeft = Math.max(0.1, (resets.getTime() - now.getTime()) / 86400000);
4
4
  const remaining = 100 - q.usedPct;
5
5
  const idealRate = remaining / daysLeft;
6
6
  const wastePct = Math.max(0, remaining - burnRate * daysLeft);
7
+ // burn > ideal is exactly "quota exhausts before reset": remaining/burn < daysLeft
8
+ const daysToExhaust = burnRate > 0 ? remaining / burnRate : Infinity;
9
+ const status = daysToExhaust < daysLeft ? "at risk" : "on track";
7
10
  let urgency = "on track";
8
11
  if (wastePct > 30 && daysLeft < 3)
9
12
  urgency = "burn now";
@@ -13,12 +16,12 @@ export function computeAdvisory(q, burnRate, now = new Date()) {
13
16
  urgency = "slow down";
14
17
  else if (wastePct > 10)
15
18
  urgency = "save";
16
- return { provider: q.provider, daysLeft, remaining, idealRate, burnRate, wastePct, urgency };
19
+ return { provider: q.provider, daysLeft, remaining, idealRate, burnRate, burnMeasured, daysToExhaust, status, wastePct, urgency };
17
20
  }
18
21
  export function recommend(quotas, _task, burnByProvider = new Map(), now = new Date()) {
19
22
  if (!quotas.length)
20
23
  return { use: "none", reason: "no quotas yet", wastePct: 0, idealRate: 0, alternatives: [], advisories: [] };
21
- const advisories = quotas.map(q => computeAdvisory(q, burnByProvider.get(q.provider) ?? 2, now));
24
+ const advisories = quotas.map(q => computeAdvisory(q, burnByProvider.get(q.provider) ?? 2, now, burnByProvider.has(q.provider)));
22
25
  const burnNow = advisories.filter(a => a.urgency === "burn now");
23
26
  const pool = burnNow.length ? burnNow : advisories;
24
27
  const use = pool.sort((a, b) => b.wastePct - a.wastePct)[0];
@@ -1,10 +1,14 @@
1
1
  export type Urgency = "burn now" | "use soon" | "slow down" | "save" | "on track";
2
+ export type BurnStatus = "at risk" | "on track";
2
3
  export interface Advisory {
3
4
  provider: string;
4
5
  daysLeft: number;
5
6
  remaining: number;
6
7
  idealRate: number;
7
8
  burnRate: number;
9
+ burnMeasured: boolean;
10
+ daysToExhaust: number;
11
+ status: BurnStatus;
8
12
  wastePct: number;
9
13
  urgency: Urgency;
10
14
  }
package/dist/cli/index.js CHANGED
@@ -17,12 +17,16 @@ program.command("status").option("--json", "json").action(async (opts) => {
17
17
  ensureDbDir();
18
18
  const db = openDb(getDbPath());
19
19
  migrate(db);
20
- const { getAllLatest } = await import("../store/quotas.js");
20
+ const { getAllLatest, getBurnRates } = await import("../store/quotas.js");
21
21
  const quotas = getAllLatest(db);
22
22
  if (opts.json)
23
23
  console.log(JSON.stringify(quotas, null, 2));
24
- else
25
- console.table(quotas);
24
+ else {
25
+ const { recommend } = await import("../advisory/engine.js");
26
+ const { renderQuotasTable } = await import("../format/table.js");
27
+ const rec = quotas.length ? recommend(quotas, "any", getBurnRates(db)) : null;
28
+ console.log(rec ? renderQuotasTable(quotas, rec.advisories) : "no quotas yet — run quotacap ingest or start the daemon");
29
+ }
26
30
  });
27
31
  program.command("advise").option("--json", "json").option("--task <t>", "task", "any").action(async (opts) => {
28
32
  ensureDbDir();
@@ -0,0 +1 @@
1
+ export declare function renderQuotasTable(quotas: any[], advisories?: any[]): string;
@@ -0,0 +1,21 @@
1
+ // The one table every surface renders: MCP tools, CLI status, web dashboard.
2
+ // Quotas carry used/resets; advisories add days-left, burn and waste analysis.
3
+ // Without advisories the analysis columns render as placeholders.
4
+ export function renderQuotasTable(quotas, advisories = []) {
5
+ const rows = quotas.map((q) => {
6
+ const a = advisories.find((x) => x.provider === q.provider);
7
+ const used = Math.round(q.usedPct ?? 0);
8
+ const resets = q.resetsAt ? new Date(q.resetsAt).toLocaleString() : "—";
9
+ const daysLeft = a?.daysLeft != null ? a.daysLeft.toFixed(1) : "—";
10
+ const ideal = a?.idealRate != null ? `${Math.round(a.idealRate)}%/day` : "—";
11
+ const burn = a?.burnMeasured ? `${a.burnRate.toFixed(1)}%/day` : a != null ? "collecting…" : "—";
12
+ const icon = a?.status === "at risk" ? "⚠️" : a != null ? "✅" : "—";
13
+ const waste = a?.wastePct != null ? `${Math.round(a.wastePct)}%` : "—";
14
+ return `| ${q.provider} | ${used}% | ${100 - used}% | ${resets} | ${daysLeft} | ${ideal} | ${burn} | ${icon} | ${waste} |`;
15
+ });
16
+ return [
17
+ "| Provider | Used | Remaining | Resets | Days left | Ideal daily burn | Burn rate | Status | Waste if unused |",
18
+ "|---|---|---|---|---|---|---|---|---|",
19
+ ...rows,
20
+ ].join("\n");
21
+ }
@@ -45,7 +45,9 @@ export function buildApp(db) {
45
45
  }
46
46
  try {
47
47
  const task = req.query?.task ?? "any";
48
- return recommend(quotas, task);
48
+ const { getBurnRates } = await import("../store/quotas.js");
49
+ const burnByProvider = getBurnRates(db);
50
+ return recommend(quotas, task, burnByProvider);
49
51
  }
50
52
  catch (e) {
51
53
  return { use: quotas[0]?.provider ?? "none", reason: `advisory error: ${e?.message ?? String(e)}`, alternatives: quotas };
@@ -37,6 +37,16 @@ export declare const tools: ({
37
37
  required: string[];
38
38
  };
39
39
  })[];
40
- export declare function recommendationTable(rec: any): string;
41
- export declare function handleTool(name: string, args: any): Promise<any>;
40
+ export declare function handleTool(name: string, args: any): Promise<{
41
+ content: {
42
+ type: string;
43
+ text: string;
44
+ }[];
45
+ quota?: undefined;
46
+ advisory?: undefined;
47
+ } | {
48
+ quota: any;
49
+ advisory: any;
50
+ content?: undefined;
51
+ }>;
42
52
  export declare function runMcpServer(): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { renderQuotasTable } from "../format/table.js";
1
2
  export const tools = [
2
3
  { name: "get_quotas", description: "All quotas with resets and health", inputSchema: { type: "object", properties: {}, required: [] } },
3
4
  { name: "get_recommendation", description: "Which provider to use next", inputSchema: { type: "object", properties: { task: { type: "string", enum: ["any", "heavy", "light"] } } } },
@@ -16,27 +17,16 @@ async function fetchJson(path) {
16
17
  throw new Error(`daemon not running, run quotacap web — ${e?.message ?? String(e)}`);
17
18
  }
18
19
  }
19
- export function recommendationTable(rec) {
20
- const rows = (rec.advisories ?? []).map((a) => {
21
- const q = (rec.alternatives ?? []).find((x) => x.provider === a.provider);
22
- const used = Math.round(q?.usedPct ?? 0);
23
- const resets = q?.resetsAt ? new Date(q.resetsAt).toLocaleString() : "—";
24
- const daysLeft = a.daysLeft != null ? a.daysLeft.toFixed(1) : "—";
25
- const ideal = a.idealRate != null ? `${Math.round(a.idealRate)}%/day` : "—";
26
- return `| ${a.provider} | ${used}% | ${100 - used}% | ${resets} | ${daysLeft} | ${ideal} | ${Math.round(a.wastePct)}% |`;
27
- });
28
- return [
29
- "| Provider | Used | Remaining | Resets | Days left | Ideal daily burn | Waste if unused |",
30
- "|---|---|---|---|---|---|---|",
31
- ...rows,
32
- ].join("\n");
33
- }
34
20
  export async function handleTool(name, args) {
35
- if (name === "get_quotas")
36
- return fetchJson("/api/quotas");
21
+ if (name === "get_quotas") {
22
+ const [quotas, rec] = await Promise.all([fetchJson("/api/quotas"), fetchJson("/api/recommendation")]);
23
+ const table = renderQuotasTable(quotas, rec?.advisories ?? []);
24
+ const json = JSON.stringify(quotas, null, 2);
25
+ return { content: [{ type: "text", text: table }, { type: "text", text: json }] };
26
+ }
37
27
  if (name === "get_recommendation") {
38
28
  const rec = await fetchJson(`/api/recommendation?task=${args?.task ?? "any"}`);
39
- const table = recommendationTable(rec);
29
+ const table = renderQuotasTable(rec.alternatives ?? [], rec.advisories ?? []);
40
30
  const json = JSON.stringify(rec, null, 2);
41
31
  return { content: [{ type: "text", text: table }, { type: "text", text: json }] };
42
32
  }
@@ -1,6 +1,6 @@
1
1
  import type { Quota } from "../adapters/types.js";
2
2
  import type { Advisory } from "./types.js";
3
- export declare function computeAdvisory(q: Quota, burnRate: number, now?: Date): Advisory;
3
+ export declare function computeAdvisory(q: Quota, burnRate: number, now?: Date, burnMeasured?: boolean): Advisory;
4
4
  export declare function recommend(quotas: Quota[], _task: string, burnByProvider?: Map<string, number>, now?: Date): {
5
5
  use: string;
6
6
  reason: string;
@@ -1,9 +1,12 @@
1
- export function computeAdvisory(q, burnRate, now = new Date()) {
1
+ export function computeAdvisory(q, burnRate, now = new Date(), burnMeasured = false) {
2
2
  const resets = new Date(q.resetsAt);
3
3
  const daysLeft = Math.max(0.1, (resets.getTime() - now.getTime()) / 86400000);
4
4
  const remaining = 100 - q.usedPct;
5
5
  const idealRate = remaining / daysLeft;
6
6
  const wastePct = Math.max(0, remaining - burnRate * daysLeft);
7
+ // burn > ideal is exactly "quota exhausts before reset": remaining/burn < daysLeft
8
+ const daysToExhaust = burnRate > 0 ? remaining / burnRate : Infinity;
9
+ const status = daysToExhaust < daysLeft ? "at risk" : "on track";
7
10
  let urgency = "on track";
8
11
  if (wastePct > 30 && daysLeft < 3)
9
12
  urgency = "burn now";
@@ -13,12 +16,12 @@ export function computeAdvisory(q, burnRate, now = new Date()) {
13
16
  urgency = "slow down";
14
17
  else if (wastePct > 10)
15
18
  urgency = "save";
16
- return { provider: q.provider, daysLeft, remaining, idealRate, burnRate, wastePct, urgency };
19
+ return { provider: q.provider, daysLeft, remaining, idealRate, burnRate, burnMeasured, daysToExhaust, status, wastePct, urgency };
17
20
  }
18
21
  export function recommend(quotas, _task, burnByProvider = new Map(), now = new Date()) {
19
22
  if (!quotas.length)
20
23
  return { use: "none", reason: "no quotas yet", wastePct: 0, idealRate: 0, alternatives: [], advisories: [] };
21
- const advisories = quotas.map(q => computeAdvisory(q, burnByProvider.get(q.provider) ?? 2, now));
24
+ const advisories = quotas.map(q => computeAdvisory(q, burnByProvider.get(q.provider) ?? 2, now, burnByProvider.has(q.provider)));
22
25
  const burnNow = advisories.filter(a => a.urgency === "burn now");
23
26
  const pool = burnNow.length ? burnNow : advisories;
24
27
  const use = pool.sort((a, b) => b.wastePct - a.wastePct)[0];
@@ -1,10 +1,14 @@
1
1
  export type Urgency = "burn now" | "use soon" | "slow down" | "save" | "on track";
2
+ export type BurnStatus = "at risk" | "on track";
2
3
  export interface Advisory {
3
4
  provider: string;
4
5
  daysLeft: number;
5
6
  remaining: number;
6
7
  idealRate: number;
7
8
  burnRate: number;
9
+ burnMeasured: boolean;
10
+ daysToExhaust: number;
11
+ status: BurnStatus;
8
12
  wastePct: number;
9
13
  urgency: Urgency;
10
14
  }
@@ -17,12 +17,16 @@ program.command("status").option("--json", "json").action(async (opts) => {
17
17
  ensureDbDir();
18
18
  const db = openDb(getDbPath());
19
19
  migrate(db);
20
- const { getAllLatest } = await import("../store/quotas.js");
20
+ const { getAllLatest, getBurnRates } = await import("../store/quotas.js");
21
21
  const quotas = getAllLatest(db);
22
22
  if (opts.json)
23
23
  console.log(JSON.stringify(quotas, null, 2));
24
- else
25
- console.table(quotas);
24
+ else {
25
+ const { recommend } = await import("../advisory/engine.js");
26
+ const { renderQuotasTable } = await import("../format/table.js");
27
+ const rec = quotas.length ? recommend(quotas, "any", getBurnRates(db)) : null;
28
+ console.log(rec ? renderQuotasTable(quotas, rec.advisories) : "no quotas yet — run quotacap ingest or start the daemon");
29
+ }
26
30
  });
27
31
  program.command("advise").option("--json", "json").option("--task <t>", "task", "any").action(async (opts) => {
28
32
  ensureDbDir();
@@ -0,0 +1 @@
1
+ export declare function renderQuotasTable(quotas: any[], advisories?: any[]): string;
@@ -0,0 +1,21 @@
1
+ // The one table every surface renders: MCP tools, CLI status, web dashboard.
2
+ // Quotas carry used/resets; advisories add days-left, burn and waste analysis.
3
+ // Without advisories the analysis columns render as placeholders.
4
+ export function renderQuotasTable(quotas, advisories = []) {
5
+ const rows = quotas.map((q) => {
6
+ const a = advisories.find((x) => x.provider === q.provider);
7
+ const used = Math.round(q.usedPct ?? 0);
8
+ const resets = q.resetsAt ? new Date(q.resetsAt).toLocaleString() : "—";
9
+ const daysLeft = a?.daysLeft != null ? a.daysLeft.toFixed(1) : "—";
10
+ const ideal = a?.idealRate != null ? `${Math.round(a.idealRate)}%/day` : "—";
11
+ const burn = a?.burnMeasured ? `${a.burnRate.toFixed(1)}%/day` : a != null ? "collecting…" : "—";
12
+ const icon = a?.status === "at risk" ? "⚠️" : a != null ? "✅" : "—";
13
+ const waste = a?.wastePct != null ? `${Math.round(a.wastePct)}%` : "—";
14
+ return `| ${q.provider} | ${used}% | ${100 - used}% | ${resets} | ${daysLeft} | ${ideal} | ${burn} | ${icon} | ${waste} |`;
15
+ });
16
+ return [
17
+ "| Provider | Used | Remaining | Resets | Days left | Ideal daily burn | Burn rate | Status | Waste if unused |",
18
+ "|---|---|---|---|---|---|---|---|---|",
19
+ ...rows,
20
+ ].join("\n");
21
+ }
@@ -45,7 +45,9 @@ export function buildApp(db) {
45
45
  }
46
46
  try {
47
47
  const task = req.query?.task ?? "any";
48
- return recommend(quotas, task);
48
+ const { getBurnRates } = await import("../store/quotas.js");
49
+ const burnByProvider = getBurnRates(db);
50
+ return recommend(quotas, task, burnByProvider);
49
51
  }
50
52
  catch (e) {
51
53
  return { use: quotas[0]?.provider ?? "none", reason: `advisory error: ${e?.message ?? String(e)}`, alternatives: quotas };
@@ -37,6 +37,16 @@ export declare const tools: ({
37
37
  required: string[];
38
38
  };
39
39
  })[];
40
- export declare function recommendationTable(rec: any): string;
41
- export declare function handleTool(name: string, args: any): Promise<any>;
40
+ export declare function handleTool(name: string, args: any): Promise<{
41
+ content: {
42
+ type: string;
43
+ text: string;
44
+ }[];
45
+ quota?: undefined;
46
+ advisory?: undefined;
47
+ } | {
48
+ quota: any;
49
+ advisory: any;
50
+ content?: undefined;
51
+ }>;
42
52
  export declare function runMcpServer(): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { renderQuotasTable } from "../format/table.js";
1
2
  export const tools = [
2
3
  { name: "get_quotas", description: "All quotas with resets and health", inputSchema: { type: "object", properties: {}, required: [] } },
3
4
  { name: "get_recommendation", description: "Which provider to use next", inputSchema: { type: "object", properties: { task: { type: "string", enum: ["any", "heavy", "light"] } } } },
@@ -16,27 +17,16 @@ async function fetchJson(path) {
16
17
  throw new Error(`daemon not running, run quotacap web — ${e?.message ?? String(e)}`);
17
18
  }
18
19
  }
19
- export function recommendationTable(rec) {
20
- const rows = (rec.advisories ?? []).map((a) => {
21
- const q = (rec.alternatives ?? []).find((x) => x.provider === a.provider);
22
- const used = Math.round(q?.usedPct ?? 0);
23
- const resets = q?.resetsAt ? new Date(q.resetsAt).toLocaleString() : "—";
24
- const daysLeft = a.daysLeft != null ? a.daysLeft.toFixed(1) : "—";
25
- const ideal = a.idealRate != null ? `${Math.round(a.idealRate)}%/day` : "—";
26
- return `| ${a.provider} | ${used}% | ${100 - used}% | ${resets} | ${daysLeft} | ${ideal} | ${Math.round(a.wastePct)}% |`;
27
- });
28
- return [
29
- "| Provider | Used | Remaining | Resets | Days left | Ideal daily burn | Waste if unused |",
30
- "|---|---|---|---|---|---|---|",
31
- ...rows,
32
- ].join("\n");
33
- }
34
20
  export async function handleTool(name, args) {
35
- if (name === "get_quotas")
36
- return fetchJson("/api/quotas");
21
+ if (name === "get_quotas") {
22
+ const [quotas, rec] = await Promise.all([fetchJson("/api/quotas"), fetchJson("/api/recommendation")]);
23
+ const table = renderQuotasTable(quotas, rec?.advisories ?? []);
24
+ const json = JSON.stringify(quotas, null, 2);
25
+ return { content: [{ type: "text", text: table }, { type: "text", text: json }] };
26
+ }
37
27
  if (name === "get_recommendation") {
38
28
  const rec = await fetchJson(`/api/recommendation?task=${args?.task ?? "any"}`);
39
- const table = recommendationTable(rec);
29
+ const table = renderQuotasTable(rec.alternatives ?? [], rec.advisories ?? []);
40
30
  const json = JSON.stringify(rec, null, 2);
41
31
  return { content: [{ type: "text", text: table }, { type: "text", text: json }] };
42
32
  }
@@ -3,3 +3,4 @@ export declare function getLatestByProvider(db: any, provider: string): any;
3
3
  export declare function getAllLatest(db: any): any;
4
4
  export declare const getQuotas: typeof getAllLatest;
5
5
  export declare function getSnapshots(db: any): any;
6
+ export declare function getBurnRates(db: any): Map<string, number>;
@@ -26,3 +26,27 @@ export function getAllLatest(db) {
26
26
  // alias for plan's getQuotas naming
27
27
  export const getQuotas = getAllLatest;
28
28
  export function getSnapshots(db) { return db.prepare(`SELECT * FROM snapshots ORDER BY day DESC`).all(); }
29
+ export function getBurnRates(db) {
30
+ const rows = db.prepare(`SELECT day, provider, used_pct FROM snapshots`).all();
31
+ const byProvider = new Map();
32
+ for (const r of rows) {
33
+ const pts = byProvider.get(r.provider) ?? [];
34
+ pts.push({ day: r.day, usedPct: r.used_pct });
35
+ byProvider.set(r.provider, pts);
36
+ }
37
+ const out = new Map();
38
+ for (const [provider, pts] of byProvider) {
39
+ if (pts.length < 2)
40
+ continue;
41
+ const sorted = [...pts].sort((a, b) => a.day.localeCompare(b.day));
42
+ const first = sorted[0];
43
+ const last = sorted[sorted.length - 1];
44
+ const days = (new Date(last.day).getTime() - new Date(first.day).getTime()) / 86400000;
45
+ if (days < 1)
46
+ continue;
47
+ const burn = (last.usedPct - first.usedPct) / days;
48
+ if (burn >= 0)
49
+ out.set(provider, burn);
50
+ }
51
+ return out;
52
+ }
@@ -1,2 +1,2 @@
1
1
  // generated by scripts/build-embed.mjs — do not edit
2
- export const VERSION = "0.0.7";
2
+ export const VERSION = "0.0.9";