@swarmdo/cli 1.29.0 → 1.30.0
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/.claude/helpers/auto-memory-hook.mjs +65 -32
- package/README.md +2 -2
- package/dist/src/commands/usage.d.ts.map +1 -1
- package/dist/src/commands/usage.js +174 -2
- package/dist/src/commands/usage.js.map +1 -1
- package/dist/src/usage/limits.d.ts +77 -0
- package/dist/src/usage/limits.d.ts.map +1 -0
- package/dist/src/usage/limits.js +134 -0
- package/dist/src/usage/limits.js.map +1 -0
- package/dist/src/usage/reflect-html.d.ts +23 -0
- package/dist/src/usage/reflect-html.d.ts.map +1 -0
- package/dist/src/usage/reflect-html.js +94 -0
- package/dist/src/usage/reflect-html.js.map +1 -0
- package/dist/src/usage/reflect.d.ts +105 -0
- package/dist/src/usage/reflect.d.ts.map +1 -0
- package/dist/src/usage/reflect.js +165 -0
- package/dist/src/usage/reflect.js.map +1 -0
- package/dist/src/usage/spend-forecast.d.ts +28 -0
- package/dist/src/usage/spend-forecast.d.ts.map +1 -0
- package/dist/src/usage/spend-forecast.js +33 -0
- package/dist/src/usage/spend-forecast.js.map +1 -0
- package/dist/src/usage/transcript-errors.d.ts +14 -0
- package/dist/src/usage/transcript-errors.d.ts.map +1 -1
- package/dist/src/usage/transcript-errors.js +10 -0
- package/dist/src/usage/transcript-errors.js.map +1 -1
- package/dist/src/util/csv.d.ts +14 -0
- package/dist/src/util/csv.d.ts.map +1 -0
- package/dist/src/util/csv.js +20 -0
- package/dist/src/util/csv.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* limits.ts — forecast when a Claude Code usage window will hit its cap.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code passes an official `rate_limits` payload to statusline scripts:
|
|
5
|
+
* for the rolling 5-hour and 7-day windows it reports `used_percentage` and
|
|
6
|
+
* `resets_at`. This is the projection layer on top — at the current burn rate,
|
|
7
|
+
* when will the window hit 100%, and does that land before the window resets?
|
|
8
|
+
* ("at this pace you hit the weekly cap Thu 14:00, 6h before it resets").
|
|
9
|
+
*
|
|
10
|
+
* Pure + deterministic: it takes a normalized window state + an injected `now`
|
|
11
|
+
* (the statusline shim maps the raw payload to WindowState — see #46), so the
|
|
12
|
+
* projection math is unit-tested with zero clock or wire-format coupling.
|
|
13
|
+
*/
|
|
14
|
+
/** Rolling-window durations Claude Code reports (5-hour + 7-day caps). */
|
|
15
|
+
export const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
|
|
16
|
+
export const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
17
|
+
/** Coerce an epoch-seconds, epoch-ms, or ISO-string timestamp to epoch ms. */
|
|
18
|
+
function toEpochMs(v) {
|
|
19
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
20
|
+
return v < 1e12 ? v * 1000 : v; // secs vs ms
|
|
21
|
+
if (typeof v === 'string') {
|
|
22
|
+
const t = Date.parse(v);
|
|
23
|
+
return Number.isNaN(t) ? null : t;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function pickWindow(obj, windowMs, label) {
|
|
28
|
+
if (!obj || typeof obj !== 'object')
|
|
29
|
+
return null;
|
|
30
|
+
const o = obj;
|
|
31
|
+
const used = (o.used_percentage ?? o.usedPercentage);
|
|
32
|
+
const resetsAtMs = toEpochMs(o.resets_at ?? o.resetsAt);
|
|
33
|
+
if (typeof used !== 'number' || !Number.isFinite(used) || resetsAtMs === null)
|
|
34
|
+
return null;
|
|
35
|
+
return { label, state: { usedPercentage: Math.max(0, Math.min(100, used)), windowMs, resetsAtMs } };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Map Claude Code's statusline `rate_limits` payload to normalized windows.
|
|
39
|
+
* Tolerant of shape drift: accepts the payload bare or wrapped in `rate_limits`,
|
|
40
|
+
* snake_case or camelCase keys, and epoch-seconds / epoch-ms / ISO `resets_at`.
|
|
41
|
+
* Windows missing usage or a reset time are dropped. Pure.
|
|
42
|
+
*/
|
|
43
|
+
export function parseRateLimits(payload) {
|
|
44
|
+
const p = payload && typeof payload === 'object' ? payload : {};
|
|
45
|
+
const rl = (p.rate_limits ?? p.rateLimits ?? p);
|
|
46
|
+
const out = [];
|
|
47
|
+
const w5 = pickWindow(rl.five_hour ?? rl.fiveHour, FIVE_HOUR_MS, '5h');
|
|
48
|
+
const w7 = pickWindow(rl.seven_day ?? rl.sevenDay, SEVEN_DAY_MS, '7d');
|
|
49
|
+
if (w5)
|
|
50
|
+
out.push(w5);
|
|
51
|
+
if (w7)
|
|
52
|
+
out.push(w7);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Forecast a single window. Burn rate is `usedPercentage / elapsed`, where
|
|
57
|
+
* elapsed = now − windowStart and windowStart = resetsAt − windowMs. Pure.
|
|
58
|
+
*/
|
|
59
|
+
export function forecastWindow(w, nowMs, opts = {}) {
|
|
60
|
+
const warnPct = opts.warnPct ?? 80;
|
|
61
|
+
const used = w.usedPercentage;
|
|
62
|
+
const msToReset = Math.max(0, w.resetsAtMs - nowMs);
|
|
63
|
+
const windowStartMs = w.resetsAtMs - w.windowMs;
|
|
64
|
+
const elapsed = nowMs - windowStartMs;
|
|
65
|
+
let exhaustionMs = null;
|
|
66
|
+
if (used >= 100) {
|
|
67
|
+
// already exhausted — mark exhaustion as "now"
|
|
68
|
+
exhaustionMs = nowMs;
|
|
69
|
+
}
|
|
70
|
+
else if (used > 0 && elapsed > 0) {
|
|
71
|
+
const msTo100 = ((100 - used) * elapsed) / used; // (remaining%) / (used%/elapsed)
|
|
72
|
+
exhaustionMs = nowMs + msTo100;
|
|
73
|
+
}
|
|
74
|
+
const willExhaust = exhaustionMs !== null && exhaustionMs <= w.resetsAtMs;
|
|
75
|
+
const status = used >= 100 ? 'over' : (willExhaust || used >= warnPct) ? 'warn' : 'ok';
|
|
76
|
+
return { usedPercentage: used, resetsAtMs: w.resetsAtMs, msToReset, exhaustionMs, willExhaust, status };
|
|
77
|
+
}
|
|
78
|
+
/** Worst status across windows (over > warn > ok). Pure. */
|
|
79
|
+
export function worstStatus(forecasts) {
|
|
80
|
+
const rank = { ok: 0, warn: 1, over: 2 };
|
|
81
|
+
return forecasts.reduce((acc, f) => (rank[f.status] > rank[acc] ? f.status : acc), 'ok');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The binding window — the one that constrains you first. A window projected to
|
|
85
|
+
* exhaust before reset outranks one that won't; among those that will, the
|
|
86
|
+
* earlier exhaustion wins; otherwise the higher used_percentage. null for [].
|
|
87
|
+
*/
|
|
88
|
+
export function bindingWindow(forecasts) {
|
|
89
|
+
if (forecasts.length === 0)
|
|
90
|
+
return null;
|
|
91
|
+
return [...forecasts].sort((a, b) => {
|
|
92
|
+
if (a.willExhaust !== b.willExhaust)
|
|
93
|
+
return a.willExhaust ? -1 : 1;
|
|
94
|
+
if (a.willExhaust && b.willExhaust)
|
|
95
|
+
return a.exhaustionMs - b.exhaustionMs;
|
|
96
|
+
return b.usedPercentage - a.usedPercentage;
|
|
97
|
+
})[0];
|
|
98
|
+
}
|
|
99
|
+
/** Compact human duration for a ms span, e.g. 9000000 → "2h30m", 90000 → "1m". */
|
|
100
|
+
export function humanizeMs(ms) {
|
|
101
|
+
if (ms <= 0)
|
|
102
|
+
return '0m';
|
|
103
|
+
const totalMin = Math.floor(ms / 60_000);
|
|
104
|
+
const d = Math.floor(totalMin / 1440);
|
|
105
|
+
const h = Math.floor((totalMin % 1440) / 60);
|
|
106
|
+
const m = totalMin % 60;
|
|
107
|
+
if (d > 0)
|
|
108
|
+
return `${d}d${h > 0 ? `${h}h` : ''}`;
|
|
109
|
+
if (h > 0)
|
|
110
|
+
return `${h}h${m > 0 ? `${m}m` : ''}`;
|
|
111
|
+
return `${m}m`;
|
|
112
|
+
}
|
|
113
|
+
/** Ultra-compact one-line indicator for a statusline, e.g. "5h 72%⚠ · 7d 12%".
|
|
114
|
+
* `!` marks an over-cap window, `⚠` an at-risk one. Pure. */
|
|
115
|
+
export function formatLimitSegment(forecasts) {
|
|
116
|
+
if (forecasts.length === 0)
|
|
117
|
+
return 'limits: n/a';
|
|
118
|
+
const mark = (s) => (s === 'over' ? '!' : s === 'warn' ? '⚠' : '');
|
|
119
|
+
return forecasts.map(({ label, f }) => `${label} ${Math.round(f.usedPercentage)}%${mark(f.status)}`).join(' · ');
|
|
120
|
+
}
|
|
121
|
+
/** One-line summary for a window, e.g. "5h window: 42% used, resets in 2h14m —
|
|
122
|
+
* on pace to hit the cap in ~1h20m (before reset)". `label` names the window. */
|
|
123
|
+
export function formatForecast(label, f, nowMs) {
|
|
124
|
+
const head = `${label}: ${Math.round(f.usedPercentage)}% used, resets in ${humanizeMs(f.msToReset)}`;
|
|
125
|
+
if (f.status === 'over')
|
|
126
|
+
return `${head} — CAP REACHED`;
|
|
127
|
+
if (f.willExhaust && f.exhaustionMs !== null) {
|
|
128
|
+
const inMs = Math.max(0, f.exhaustionMs - nowMs);
|
|
129
|
+
const before = humanizeMs(f.resetsAtMs - f.exhaustionMs);
|
|
130
|
+
return `${head} — on pace to hit the cap in ~${humanizeMs(inMs)} (${before} before reset)`;
|
|
131
|
+
}
|
|
132
|
+
return `${head} — on pace to stay under the cap`;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../../../src/usage/limits.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reflect-html.ts — render a Reflection (see reflect.ts) into a self-contained,
|
|
3
|
+
* shareable HTML dashboard for `swarmdo usage reflect --html`.
|
|
4
|
+
*
|
|
5
|
+
* Pure + deterministic: it takes an already-computed Reflection and returns a
|
|
6
|
+
* complete HTML document string with all CSS inlined (no external requests, no
|
|
7
|
+
* clock — any timestamp is passed in), so it unit-tests without a browser or a
|
|
8
|
+
* network. Every user-controlled string (model ids, project paths) is HTML-
|
|
9
|
+
* escaped. See #47.
|
|
10
|
+
*/
|
|
11
|
+
import type { Reflection } from './reflect.js';
|
|
12
|
+
/** HTML-escape a string for safe interpolation into text or attributes. */
|
|
13
|
+
export declare function escapeHtml(s: string): string;
|
|
14
|
+
/** Render the retrospective to a complete self-contained HTML document. Pure. */
|
|
15
|
+
export declare function renderReflectionHtml(r: Reflection, opts?: {
|
|
16
|
+
generatedAt?: string;
|
|
17
|
+
delegation?: {
|
|
18
|
+
taskCalls: number;
|
|
19
|
+
toolCalls: number;
|
|
20
|
+
ratio: number;
|
|
21
|
+
};
|
|
22
|
+
}): string;
|
|
23
|
+
//# sourceMappingURL=reflect-html.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reflect-html.d.ts","sourceRoot":"","sources":["../../../src/usage/reflect-html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5C;AAoCD,iFAAiF;AACjF,wBAAgB,oBAAoB,CAClC,CAAC,EAAE,UAAU,EACb,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAAO,GACxG,MAAM,CA+CR"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reflect-html.ts — render a Reflection (see reflect.ts) into a self-contained,
|
|
3
|
+
* shareable HTML dashboard for `swarmdo usage reflect --html`.
|
|
4
|
+
*
|
|
5
|
+
* Pure + deterministic: it takes an already-computed Reflection and returns a
|
|
6
|
+
* complete HTML document string with all CSS inlined (no external requests, no
|
|
7
|
+
* clock — any timestamp is passed in), so it unit-tests without a browser or a
|
|
8
|
+
* network. Every user-controlled string (model ids, project paths) is HTML-
|
|
9
|
+
* escaped. See #47.
|
|
10
|
+
*/
|
|
11
|
+
/** HTML-escape a string for safe interpolation into text or attributes. */
|
|
12
|
+
export function escapeHtml(s) {
|
|
13
|
+
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
14
|
+
}
|
|
15
|
+
const usd = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
16
|
+
const num = (n) => n.toLocaleString('en-US');
|
|
17
|
+
const pct = (x) => `${Math.round(x * 100)}%`;
|
|
18
|
+
/** A labelled proportional bar (width is % of the largest value in its group). */
|
|
19
|
+
function bar(label, valueText, fillPct) {
|
|
20
|
+
const w = Math.max(0, Math.min(100, fillPct));
|
|
21
|
+
return `<div class="bar"><div class="bar-label" title="${escapeHtml(label)}">${escapeHtml(label)}</div>`
|
|
22
|
+
+ `<div class="bar-track"><div class="bar-fill" style="width:${w.toFixed(1)}%"></div></div>`
|
|
23
|
+
+ `<div class="bar-val">${escapeHtml(valueText)}</div></div>`;
|
|
24
|
+
}
|
|
25
|
+
function shareBars(rows) {
|
|
26
|
+
if (!rows.length)
|
|
27
|
+
return '<p class="empty">—</p>';
|
|
28
|
+
const max = Math.max(...rows.map((r) => r.costUsd), 0) || 1;
|
|
29
|
+
return rows.map((r) => bar(r.model, `${usd(r.costUsd)} · ${pct(r.pct)}`, (r.costUsd / max) * 100)).join('');
|
|
30
|
+
}
|
|
31
|
+
function hourBars(hist) {
|
|
32
|
+
if (!hist.some((v) => v > 0))
|
|
33
|
+
return '<p class="empty">—</p>';
|
|
34
|
+
const max = Math.max(...hist) || 1;
|
|
35
|
+
const cells = hist.map((v, h) => {
|
|
36
|
+
const hpct = (v / max) * 100;
|
|
37
|
+
return `<div class="hour" title="${String(h).padStart(2, '0')}:00 — ${usd(v)}">`
|
|
38
|
+
+ `<div class="hour-bar" style="height:${hpct.toFixed(1)}%"></div>`
|
|
39
|
+
+ `<div class="hour-tick">${h % 6 === 0 ? String(h).padStart(2, '0') : ''}</div></div>`;
|
|
40
|
+
}).join('');
|
|
41
|
+
return `<div class="hours">${cells}</div>`;
|
|
42
|
+
}
|
|
43
|
+
function stat(label, value) {
|
|
44
|
+
return `<div class="stat"><div class="stat-val">${escapeHtml(value)}</div><div class="stat-label">${escapeHtml(label)}</div></div>`;
|
|
45
|
+
}
|
|
46
|
+
/** Render the retrospective to a complete self-contained HTML document. Pure. */
|
|
47
|
+
export function renderReflectionHtml(r, opts = {}) {
|
|
48
|
+
const arrow = r.trend.direction === 'up' ? '↑' : r.trend.direction === 'down' ? '↓' : '→';
|
|
49
|
+
const peak = r.peakHour ? `${String(r.peakHour.hour).padStart(2, '0')}:00` : '—';
|
|
50
|
+
const busiest = r.busiestDay ? `${r.busiestDay.day} (${usd(r.busiestDay.costUsd)})` : '—';
|
|
51
|
+
const gen = opts.generatedAt ? `<p class="gen">generated ${escapeHtml(opts.generatedAt)}</p>` : '';
|
|
52
|
+
return `<!doctype html>
|
|
53
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
54
|
+
<title>Claude Code Reflect — ${escapeHtml(r.period.from)}..${escapeHtml(r.period.to)}</title>
|
|
55
|
+
<style>
|
|
56
|
+
:root{--bg:#0f1117;--card:#181b24;--fg:#e6e8ee;--muted:#8b90a0;--accent:#7c9cff;--track:#262a36}
|
|
57
|
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;padding:32px}
|
|
58
|
+
.wrap{max-width:840px;margin:0 auto}
|
|
59
|
+
h1{font-size:22px;margin:0 0 2px}.sub{color:var(--muted);margin:0 0 24px}.gen{color:var(--muted);font-size:12px;margin:24px 0 0}
|
|
60
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:28px}
|
|
61
|
+
.stat{background:var(--card);border-radius:12px;padding:16px}
|
|
62
|
+
.stat-val{font-size:22px;font-weight:600}.stat-label{color:var(--muted);font-size:12px;margin-top:4px}
|
|
63
|
+
h2{font-size:14px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin:28px 0 12px}
|
|
64
|
+
.bar{display:grid;grid-template-columns:minmax(0,1fr) 3fr minmax(0,auto);align-items:center;gap:10px;margin:6px 0}
|
|
65
|
+
.bar-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}
|
|
66
|
+
.bar-track{background:var(--track);border-radius:6px;height:14px;overflow:hidden}
|
|
67
|
+
.bar-fill{background:var(--accent);height:100%;border-radius:6px}
|
|
68
|
+
.bar-val{font-size:12px;color:var(--muted);white-space:nowrap}
|
|
69
|
+
.hours{display:flex;align-items:flex-end;gap:3px;height:90px}
|
|
70
|
+
.hour{flex:1;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;height:100%}
|
|
71
|
+
.hour-bar{width:100%;background:var(--accent);border-radius:3px 3px 0 0;min-height:1px}
|
|
72
|
+
.hour-tick{font-size:9px;color:var(--muted);margin-top:4px;height:12px}
|
|
73
|
+
.empty{color:var(--muted)}
|
|
74
|
+
</style></head><body><div class="wrap">
|
|
75
|
+
<h1>Claude Code Reflect ${escapeHtml(arrow)}</h1>
|
|
76
|
+
<p class="sub">${escapeHtml(r.period.from)} .. ${escapeHtml(r.period.to)} · ${r.period.spanDays} days · ${r.totals.activeDays} active</p>
|
|
77
|
+
<div class="grid">
|
|
78
|
+
${stat('Total spend', usd(r.totals.costUsd))}
|
|
79
|
+
${stat('Total tokens', num(r.totals.totalTokens))}
|
|
80
|
+
${stat('Avg / active day', usd(r.avgCostPerActiveDay))}
|
|
81
|
+
${stat('Longest streak', `${r.longestStreak} day${r.longestStreak === 1 ? '' : 's'}`)}
|
|
82
|
+
${stat('Peak hour', peak)}
|
|
83
|
+
${stat('Cache read', pct(r.cacheReadPct))}
|
|
84
|
+
${stat('Busiest day', busiest)}
|
|
85
|
+
${stat('Trend', `${arrow} ${pct(r.trend.firstHalfCost > 0 ? (r.trend.secondHalfCost - r.trend.firstHalfCost) / r.trend.firstHalfCost : 0)}`)}
|
|
86
|
+
${opts.delegation && opts.delegation.toolCalls > 0 ? stat('Delegation', pct(opts.delegation.ratio)) : ''}
|
|
87
|
+
</div>
|
|
88
|
+
<h2>Cost by hour of day</h2>${hourBars(r.hourHistogram)}
|
|
89
|
+
<h2>Top models</h2>${shareBars(r.topModels)}
|
|
90
|
+
<h2>Top projects</h2>${shareBars(r.topProjects)}
|
|
91
|
+
${gen}
|
|
92
|
+
</div></body></html>`;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=reflect-html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reflect-html.js","sourceRoot":"","sources":["../../../src/usage/reflect-html.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reflect.ts — a "wrapped"-style retrospective over Claude Code usage.
|
|
3
|
+
*
|
|
4
|
+
* Anthropic's Reflect (2026-07-09) summarizes claude.ai *chats*; it doesn't
|
|
5
|
+
* touch Claude Code terminal work. swarmdo already parses the local transcripts
|
|
6
|
+
* for `usage daily/monthly/blocks/errors/cache`, so this is the retrospective
|
|
7
|
+
* layer on top: fold the existing per-day and per-(model,day) aggregation rows
|
|
8
|
+
* into headline stats — totals, the busiest day, the top models, the longest
|
|
9
|
+
* active-day streak, the cost trend, and cache efficiency.
|
|
10
|
+
*
|
|
11
|
+
* Pure + deterministic: it takes already-aggregated rows (no transcripts, no
|
|
12
|
+
* clock — the period bounds are passed in), so the whole thing is unit-tested
|
|
13
|
+
* without touching disk. The command layer in ../commands/usage.ts feeds it the
|
|
14
|
+
* rows the parser already produces. See #47.
|
|
15
|
+
*/
|
|
16
|
+
import type { DayRow, ModelRow } from './diff.js';
|
|
17
|
+
export interface ModelShare {
|
|
18
|
+
model: string;
|
|
19
|
+
costUsd: number;
|
|
20
|
+
totalTokens: number;
|
|
21
|
+
/** share of total cost, 0..1 (0 when the period cost is 0) */
|
|
22
|
+
pct: number;
|
|
23
|
+
}
|
|
24
|
+
export interface ReflectionTotals {
|
|
25
|
+
costUsd: number;
|
|
26
|
+
totalTokens: number;
|
|
27
|
+
inputTokens: number;
|
|
28
|
+
outputTokens: number;
|
|
29
|
+
cacheReadTokens: number;
|
|
30
|
+
cacheWriteTokens: number;
|
|
31
|
+
entries: number;
|
|
32
|
+
/** days in [from,to] with any token activity */
|
|
33
|
+
activeDays: number;
|
|
34
|
+
}
|
|
35
|
+
export interface Reflection {
|
|
36
|
+
period: {
|
|
37
|
+
from: string;
|
|
38
|
+
to: string; /** inclusive calendar span in days */
|
|
39
|
+
spanDays: number;
|
|
40
|
+
};
|
|
41
|
+
totals: ReflectionTotals;
|
|
42
|
+
/** highest-cost active day in the period, or null if the period is empty */
|
|
43
|
+
busiestDay: {
|
|
44
|
+
day: string;
|
|
45
|
+
costUsd: number;
|
|
46
|
+
totalTokens: number;
|
|
47
|
+
} | null;
|
|
48
|
+
/** models ranked by cost desc (capped to opts.topModels) */
|
|
49
|
+
topModels: ModelShare[];
|
|
50
|
+
/** projects ranked by cost desc (ModelShare.model holds the project path) */
|
|
51
|
+
topProjects: ModelShare[];
|
|
52
|
+
/** busiest local hour-of-day by cost (0..23), or null if no activity */
|
|
53
|
+
peakHour: {
|
|
54
|
+
hour: number;
|
|
55
|
+
value: number;
|
|
56
|
+
} | null;
|
|
57
|
+
/** cost per local hour-of-day, as supplied by the caller (length 24 when set) */
|
|
58
|
+
hourHistogram: number[];
|
|
59
|
+
/** longest run of consecutive active calendar days */
|
|
60
|
+
longestStreak: number;
|
|
61
|
+
/** cacheRead / (input + cacheWrite + cacheRead), 0..1 */
|
|
62
|
+
cacheReadPct: number;
|
|
63
|
+
/** total cost / active days (0 when no active days) */
|
|
64
|
+
avgCostPerActiveDay: number;
|
|
65
|
+
/** first-half vs second-half spend across the period */
|
|
66
|
+
trend: {
|
|
67
|
+
firstHalfCost: number;
|
|
68
|
+
secondHalfCost: number;
|
|
69
|
+
direction: 'up' | 'down' | 'flat';
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export interface ReflectOptions {
|
|
73
|
+
/** how many models to keep in topModels (default 5) */
|
|
74
|
+
topModels?: number;
|
|
75
|
+
/** relative change below which the trend reads 'flat' (default 0.05 = 5%) */
|
|
76
|
+
flatThreshold?: number;
|
|
77
|
+
}
|
|
78
|
+
/** The calendar day after an ISO date (handles month/year rollover). Pure. */
|
|
79
|
+
export declare function nextDay(iso: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* The ISO date `n` whole months before `iso`, clamping the day to the target
|
|
82
|
+
* month's length so "Mar 31 − 1 month" is Feb 28/29 (not a rolled-over Mar 3).
|
|
83
|
+
* Pure. Used to derive a `--period 1m|3m|…` window start from today.
|
|
84
|
+
*/
|
|
85
|
+
export declare function monthsBefore(iso: string, n: number): string;
|
|
86
|
+
/** Inclusive whole-day span between two ISO dates (from <= to). Pure. */
|
|
87
|
+
export declare function spanDays(from: string, to: string): number;
|
|
88
|
+
/** Longest run of consecutive calendar days present in the set. Pure. */
|
|
89
|
+
export declare function longestStreakOf(days: string[]): number;
|
|
90
|
+
/** Render a numeric series as a Unicode block sparkline (one char per bucket).
|
|
91
|
+
* Zero buckets are blank; the rest scale ▁..█ against the max. Pure — used for
|
|
92
|
+
* the terminal cost-by-hour view (the HTML dashboard draws the same data). */
|
|
93
|
+
export declare function hourSparkline(hist: number[]): string;
|
|
94
|
+
/** Window + aggregate `{key, day, totals}` rows into cost-ranked shares. Pure.
|
|
95
|
+
* Reused for both models and projects (ModelShare.model carries whichever key). */
|
|
96
|
+
export declare function rankShares(rows: ModelRow[], from: string, to: string, totalCost: number, topN: number): ModelShare[];
|
|
97
|
+
/** Busiest bucket in a per-hour cost histogram (argmax; earliest hour wins ties),
|
|
98
|
+
* or null when every bucket is empty. Pure. */
|
|
99
|
+
export declare function peakHourOf(hourHistogram: number[]): {
|
|
100
|
+
hour: number;
|
|
101
|
+
value: number;
|
|
102
|
+
} | null;
|
|
103
|
+
/** Fold pre-aggregated usage rows into a retrospective. Pure. */
|
|
104
|
+
export declare function computeReflection(dayRows: DayRow[], modelRows: ModelRow[], from: string, to: string, opts?: ReflectOptions, projectRows?: ModelRow[], hourHistogram?: number[]): Reflection;
|
|
105
|
+
//# sourceMappingURL=reflect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reflect.d.ts","sourceRoot":"","sources":["../../../src/usage/reflect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAElD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC,CAAC,sCAAsC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9F,MAAM,EAAE,gBAAgB,CAAC;IACzB,4EAA4E;IAC5E,UAAU,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzE,4DAA4D;IAC5D,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB,6EAA6E;IAC7E,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,wEAAwE;IACxE,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD,iFAAiF;IACjF,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,sDAAsD;IACtD,aAAa,EAAE,MAAM,CAAC;IACtB,yDAAyD;IACzD,YAAY,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,KAAK,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CAC7F;AAED,MAAM,WAAW,cAAc;IAC7B,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAID,8EAA8E;AAC9E,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO3C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAS3D;AAED,yEAAyE;AACzE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAWtD;AAMD;;8EAE8E;AAC9E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAIpD;AAED;mFACmF;AACnF,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CAcpH;AAED;+CAC+C;AAC/C,wBAAgB,UAAU,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAO1F;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EAAE,EACjB,SAAS,EAAE,QAAQ,EAAE,EACrB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,IAAI,GAAE,cAAmB,EACzB,WAAW,GAAE,QAAQ,EAAO,EAC5B,aAAa,GAAE,MAAM,EAAO,GAC3B,UAAU,CA4DZ"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reflect.ts — a "wrapped"-style retrospective over Claude Code usage.
|
|
3
|
+
*
|
|
4
|
+
* Anthropic's Reflect (2026-07-09) summarizes claude.ai *chats*; it doesn't
|
|
5
|
+
* touch Claude Code terminal work. swarmdo already parses the local transcripts
|
|
6
|
+
* for `usage daily/monthly/blocks/errors/cache`, so this is the retrospective
|
|
7
|
+
* layer on top: fold the existing per-day and per-(model,day) aggregation rows
|
|
8
|
+
* into headline stats — totals, the busiest day, the top models, the longest
|
|
9
|
+
* active-day streak, the cost trend, and cache efficiency.
|
|
10
|
+
*
|
|
11
|
+
* Pure + deterministic: it takes already-aggregated rows (no transcripts, no
|
|
12
|
+
* clock — the period bounds are passed in), so the whole thing is unit-tested
|
|
13
|
+
* without touching disk. The command layer in ../commands/usage.ts feeds it the
|
|
14
|
+
* rows the parser already produces. See #47.
|
|
15
|
+
*/
|
|
16
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
17
|
+
/** The calendar day after an ISO date (handles month/year rollover). Pure. */
|
|
18
|
+
export function nextDay(iso) {
|
|
19
|
+
const [y, m, d] = iso.split('-').map(Number);
|
|
20
|
+
const dt = new Date(y, m - 1, d + 1);
|
|
21
|
+
const yy = dt.getFullYear();
|
|
22
|
+
const mm = String(dt.getMonth() + 1).padStart(2, '0');
|
|
23
|
+
const dd = String(dt.getDate()).padStart(2, '0');
|
|
24
|
+
return `${yy}-${mm}-${dd}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The ISO date `n` whole months before `iso`, clamping the day to the target
|
|
28
|
+
* month's length so "Mar 31 − 1 month" is Feb 28/29 (not a rolled-over Mar 3).
|
|
29
|
+
* Pure. Used to derive a `--period 1m|3m|…` window start from today.
|
|
30
|
+
*/
|
|
31
|
+
export function monthsBefore(iso, n) {
|
|
32
|
+
const [y, m, d] = iso.split('-').map(Number);
|
|
33
|
+
const monthIndex = m - 1 - n; // may be negative — Date normalizes the year
|
|
34
|
+
const lastDay = new Date(y, monthIndex + 1, 0).getDate();
|
|
35
|
+
const dt = new Date(y, monthIndex, Math.min(d, lastDay));
|
|
36
|
+
const yy = dt.getFullYear();
|
|
37
|
+
const mm = String(dt.getMonth() + 1).padStart(2, '0');
|
|
38
|
+
const dd = String(dt.getDate()).padStart(2, '0');
|
|
39
|
+
return `${yy}-${mm}-${dd}`;
|
|
40
|
+
}
|
|
41
|
+
/** Inclusive whole-day span between two ISO dates (from <= to). Pure. */
|
|
42
|
+
export function spanDays(from, to) {
|
|
43
|
+
const a = new Date(from + 'T00:00:00');
|
|
44
|
+
const b = new Date(to + 'T00:00:00');
|
|
45
|
+
return Math.floor((b.getTime() - a.getTime()) / 86_400_000) + 1;
|
|
46
|
+
}
|
|
47
|
+
/** Longest run of consecutive calendar days present in the set. Pure. */
|
|
48
|
+
export function longestStreakOf(days) {
|
|
49
|
+
const uniq = [...new Set(days)].sort();
|
|
50
|
+
let best = 0;
|
|
51
|
+
let run = 0;
|
|
52
|
+
let prev = null;
|
|
53
|
+
for (const d of uniq) {
|
|
54
|
+
run = prev !== null && nextDay(prev) === d ? run + 1 : 1;
|
|
55
|
+
if (run > best)
|
|
56
|
+
best = run;
|
|
57
|
+
prev = d;
|
|
58
|
+
}
|
|
59
|
+
return best;
|
|
60
|
+
}
|
|
61
|
+
const within = (key, from, to) => key >= from && key <= to;
|
|
62
|
+
const SPARK_BLOCKS = ' ▁▂▃▄▅▆▇█'; // index 0 = empty (zero), 1..8 = ▁..█ scaled to max
|
|
63
|
+
/** Render a numeric series as a Unicode block sparkline (one char per bucket).
|
|
64
|
+
* Zero buckets are blank; the rest scale ▁..█ against the max. Pure — used for
|
|
65
|
+
* the terminal cost-by-hour view (the HTML dashboard draws the same data). */
|
|
66
|
+
export function hourSparkline(hist) {
|
|
67
|
+
const max = Math.max(0, ...hist);
|
|
68
|
+
if (max <= 0)
|
|
69
|
+
return ' '.repeat(hist.length);
|
|
70
|
+
return hist.map((v) => SPARK_BLOCKS[v <= 0 ? 0 : Math.min(8, 1 + Math.floor((v / max) * 7))]).join('');
|
|
71
|
+
}
|
|
72
|
+
/** Window + aggregate `{key, day, totals}` rows into cost-ranked shares. Pure.
|
|
73
|
+
* Reused for both models and projects (ModelShare.model carries whichever key). */
|
|
74
|
+
export function rankShares(rows, from, to, totalCost, topN) {
|
|
75
|
+
const per = new Map();
|
|
76
|
+
for (const r of rows) {
|
|
77
|
+
if (!within(r.day, from, to))
|
|
78
|
+
continue;
|
|
79
|
+
const slot = per.get(r.key) ?? { costUsd: 0, totalTokens: 0 };
|
|
80
|
+
slot.costUsd += r.totals.costUsd;
|
|
81
|
+
slot.totalTokens += r.totals.totalTokens;
|
|
82
|
+
per.set(r.key, slot);
|
|
83
|
+
}
|
|
84
|
+
return [...per.entries()]
|
|
85
|
+
.map(([key, v]) => ({ model: key, costUsd: v.costUsd, totalTokens: v.totalTokens, pct: totalCost > 0 ? v.costUsd / totalCost : 0 }))
|
|
86
|
+
.filter((m) => m.costUsd > 0 || m.totalTokens > 0)
|
|
87
|
+
.sort((a, b) => b.costUsd - a.costUsd || b.totalTokens - a.totalTokens || (a.model < b.model ? -1 : 1))
|
|
88
|
+
.slice(0, topN);
|
|
89
|
+
}
|
|
90
|
+
/** Busiest bucket in a per-hour cost histogram (argmax; earliest hour wins ties),
|
|
91
|
+
* or null when every bucket is empty. Pure. */
|
|
92
|
+
export function peakHourOf(hourHistogram) {
|
|
93
|
+
let best = -1;
|
|
94
|
+
let bestVal = 0;
|
|
95
|
+
for (let h = 0; h < hourHistogram.length; h++) {
|
|
96
|
+
if (hourHistogram[h] > bestVal) {
|
|
97
|
+
bestVal = hourHistogram[h];
|
|
98
|
+
best = h;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return best < 0 ? null : { hour: best, value: bestVal };
|
|
102
|
+
}
|
|
103
|
+
/** Fold pre-aggregated usage rows into a retrospective. Pure. */
|
|
104
|
+
export function computeReflection(dayRows, modelRows, from, to, opts = {}, projectRows = [], hourHistogram = []) {
|
|
105
|
+
if (!DATE.test(from) || !DATE.test(to))
|
|
106
|
+
throw new Error(`bad period bounds: ${from}..${to}`);
|
|
107
|
+
if (from > to)
|
|
108
|
+
throw new Error(`period "${from}..${to}" is reversed (from > to)`);
|
|
109
|
+
const topN = opts.topModels ?? 5;
|
|
110
|
+
const flat = opts.flatThreshold ?? 0.05;
|
|
111
|
+
const days = dayRows.filter((r) => within(r.key, from, to));
|
|
112
|
+
const totals = {
|
|
113
|
+
costUsd: 0, totalTokens: 0, inputTokens: 0, outputTokens: 0,
|
|
114
|
+
cacheReadTokens: 0, cacheWriteTokens: 0, entries: 0, activeDays: 0,
|
|
115
|
+
};
|
|
116
|
+
let busiestDay = null;
|
|
117
|
+
const activeDayKeys = [];
|
|
118
|
+
const mid = spanDays(from, to) / 2;
|
|
119
|
+
let firstHalfCost = 0;
|
|
120
|
+
let secondHalfCost = 0;
|
|
121
|
+
for (const r of days) {
|
|
122
|
+
const t = r.totals;
|
|
123
|
+
totals.costUsd += t.costUsd;
|
|
124
|
+
totals.totalTokens += t.totalTokens;
|
|
125
|
+
totals.inputTokens += t.inputTokens;
|
|
126
|
+
totals.outputTokens += t.outputTokens;
|
|
127
|
+
totals.cacheReadTokens += t.cacheReadTokens;
|
|
128
|
+
totals.cacheWriteTokens += t.cacheWriteTokens;
|
|
129
|
+
if (t.totalTokens > 0) {
|
|
130
|
+
totals.activeDays += 1;
|
|
131
|
+
activeDayKeys.push(r.key);
|
|
132
|
+
}
|
|
133
|
+
if (!busiestDay || t.costUsd > busiestDay.costUsd) {
|
|
134
|
+
busiestDay = { day: r.key, costUsd: t.costUsd, totalTokens: t.totalTokens };
|
|
135
|
+
}
|
|
136
|
+
// Trend: which half of the period does this day fall in?
|
|
137
|
+
if (spanDays(from, r.key) <= mid)
|
|
138
|
+
firstHalfCost += t.costUsd;
|
|
139
|
+
else
|
|
140
|
+
secondHalfCost += t.costUsd;
|
|
141
|
+
}
|
|
142
|
+
// A period with zero active days has no meaningful busiest day.
|
|
143
|
+
if (totals.activeDays === 0)
|
|
144
|
+
busiestDay = null;
|
|
145
|
+
// Cost-ranked shares over the same window, for both models and projects.
|
|
146
|
+
const topModels = rankShares(modelRows, from, to, totals.costUsd, topN);
|
|
147
|
+
const topProjects = rankShares(projectRows, from, to, totals.costUsd, topN);
|
|
148
|
+
const inputSide = totals.inputTokens + totals.cacheWriteTokens + totals.cacheReadTokens;
|
|
149
|
+
const relChange = firstHalfCost > 0 ? (secondHalfCost - firstHalfCost) / firstHalfCost : (secondHalfCost > 0 ? 1 : 0);
|
|
150
|
+
const direction = Math.abs(relChange) < flat ? 'flat' : relChange > 0 ? 'up' : 'down';
|
|
151
|
+
return {
|
|
152
|
+
period: { from, to, spanDays: spanDays(from, to) },
|
|
153
|
+
totals,
|
|
154
|
+
busiestDay,
|
|
155
|
+
topModels,
|
|
156
|
+
topProjects,
|
|
157
|
+
peakHour: peakHourOf(hourHistogram),
|
|
158
|
+
hourHistogram,
|
|
159
|
+
longestStreak: longestStreakOf(activeDayKeys),
|
|
160
|
+
cacheReadPct: inputSide > 0 ? totals.cacheReadTokens / inputSide : 0,
|
|
161
|
+
avgCostPerActiveDay: totals.activeDays > 0 ? totals.costUsd / totals.activeDays : 0,
|
|
162
|
+
trend: { firstHalfCost, secondHalfCost, direction },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=reflect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reflect.js","sourceRoot":"","sources":["../../../src/usage/reflect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spend-forecast.ts — project month-end Claude Code spend from month-to-date
|
|
3
|
+
* burn, for budgeting ("on pace for ~$420 this month").
|
|
4
|
+
*
|
|
5
|
+
* The quota forecaster (limits.ts) answers "will I hit the rolling cap"; this
|
|
6
|
+
* answers "what will the calendar month cost at the current daily average".
|
|
7
|
+
* Pure + deterministic: month-to-date total, the day-of-month, and the days in
|
|
8
|
+
* the month are passed in, so it's unit-tested without a clock.
|
|
9
|
+
*/
|
|
10
|
+
export interface SpendProjection {
|
|
11
|
+
monthToDateUsd: number;
|
|
12
|
+
dayOfMonth: number;
|
|
13
|
+
daysInMonth: number;
|
|
14
|
+
/** month-to-date / day-of-month */
|
|
15
|
+
dailyAverageUsd: number;
|
|
16
|
+
/** dailyAverage × daysInMonth — projected month-end total */
|
|
17
|
+
projectedUsd: number;
|
|
18
|
+
/** projectedUsd − monthToDateUsd — the remaining projected spend */
|
|
19
|
+
remainingUsd: number;
|
|
20
|
+
}
|
|
21
|
+
/** Days in the calendar month of an ISO `YYYY-MM` (or `YYYY-MM-DD`). Pure. */
|
|
22
|
+
export declare function daysInMonthOf(iso: string): number;
|
|
23
|
+
/**
|
|
24
|
+
* Project month-end spend at the current daily average. `dayOfMonth` is how many
|
|
25
|
+
* days (inclusive of today) have accrued the month-to-date total. Pure.
|
|
26
|
+
*/
|
|
27
|
+
export declare function projectMonthEnd(monthToDateUsd: number, dayOfMonth: number, daysInMonth: number): SpendProjection;
|
|
28
|
+
//# sourceMappingURL=spend-forecast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend-forecast.d.ts","sourceRoot":"","sources":["../../../src/usage/spend-forecast.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,eAAe,EAAE,MAAM,CAAC;IACxB,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIjD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,cAAc,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,eAAe,CAWhH"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spend-forecast.ts — project month-end Claude Code spend from month-to-date
|
|
3
|
+
* burn, for budgeting ("on pace for ~$420 this month").
|
|
4
|
+
*
|
|
5
|
+
* The quota forecaster (limits.ts) answers "will I hit the rolling cap"; this
|
|
6
|
+
* answers "what will the calendar month cost at the current daily average".
|
|
7
|
+
* Pure + deterministic: month-to-date total, the day-of-month, and the days in
|
|
8
|
+
* the month are passed in, so it's unit-tested without a clock.
|
|
9
|
+
*/
|
|
10
|
+
/** Days in the calendar month of an ISO `YYYY-MM` (or `YYYY-MM-DD`). Pure. */
|
|
11
|
+
export function daysInMonthOf(iso) {
|
|
12
|
+
const [y, m] = iso.split('-').map(Number);
|
|
13
|
+
if (!Number.isFinite(y) || !Number.isFinite(m) || m < 1 || m > 12)
|
|
14
|
+
return NaN;
|
|
15
|
+
return new Date(y, m, 0).getDate(); // day 0 of the next month = last day of this one
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Project month-end spend at the current daily average. `dayOfMonth` is how many
|
|
19
|
+
* days (inclusive of today) have accrued the month-to-date total. Pure.
|
|
20
|
+
*/
|
|
21
|
+
export function projectMonthEnd(monthToDateUsd, dayOfMonth, daysInMonth) {
|
|
22
|
+
const dailyAverageUsd = dayOfMonth > 0 ? monthToDateUsd / dayOfMonth : 0;
|
|
23
|
+
const projectedUsd = dailyAverageUsd * daysInMonth;
|
|
24
|
+
return {
|
|
25
|
+
monthToDateUsd,
|
|
26
|
+
dayOfMonth,
|
|
27
|
+
daysInMonth,
|
|
28
|
+
dailyAverageUsd,
|
|
29
|
+
projectedUsd,
|
|
30
|
+
remainingUsd: Math.max(0, projectedUsd - monthToDateUsd),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=spend-forecast.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend-forecast.js","sourceRoot":"","sources":["../../../src/usage/spend-forecast.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAcH,8EAA8E;AAC9E,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,GAAG,CAAC;IAC9E,OAAO,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,iDAAiD;AACvF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,cAAsB,EAAE,UAAkB,EAAE,WAAmB;IAC7F,MAAM,eAAe,GAAG,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,cAAc,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IACzE,MAAM,YAAY,GAAG,eAAe,GAAG,WAAW,CAAC;IACnD,OAAO;QACL,cAAc;QACd,UAAU;QACV,WAAW;QACX,eAAe;QACf,YAAY;QACZ,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,GAAG,cAAc,CAAC;KACzD,CAAC;AACJ,CAAC"}
|