sprag-cli 3.58.1 → 3.59.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/bin/cli.js CHANGED
@@ -106,7 +106,7 @@ function tableColor() {
106
106
  const KNOWN_SUBCOMMANDS = new Set([
107
107
  'last', 'brief', 'history', 'handoff', 'install', 'uninstall', 'mode', 'korean', 'cohesion',
108
108
  'doc2md', 'delegate', 'harness', 'route-scan', 'compact-window', 'update-check', 'upgrade',
109
- 'seed', 'litellm-budget', 'profile-map', 'feedback',
109
+ 'seed', 'litellm-budget', 'profile-map', 'feedback', 'saved',
110
110
  ]);
111
111
 
112
112
  const USAGE = `sprag — Claude Code and Codex agent harness
@@ -148,6 +148,7 @@ Usage:
148
148
  sprag doc2md on|off|<file> convert documents to Markdown before the model reads them
149
149
  sprag delegate on|off|status add bounds and context to Task/Agent delegation prompts
150
150
  sprag compact-window audit or pin autoCompactWindow for 1M sessions
151
+ sprag saved [reset|undo] show, reset or restore the saved-money counters (--agent codex for Codex)
151
152
  sprag litellm-budget LiteLLM gateway key budget and spend
152
153
  sprag update-check is a newer version out?
153
154
  sprag brief --hook internal: the UserPromptSubmit hook entry point
@@ -305,6 +306,12 @@ async function main() {
305
306
  return (await import('../src/commands/seed.js')).run({ args, hasFlag });
306
307
  }
307
308
 
309
+ // Subcommand: saved — stopwatch reset for the lifetime "saved" counters.
310
+ // sprag saved | saved reset [routing|doc2md|all] | saved undo
311
+ if (args[0] === 'saved') {
312
+ return (await import('../src/commands/saved.js')).run({ args, hasFlag });
313
+ }
314
+
308
315
  // Subcommand: harness — manage the project's CLAUDE.md harness rules.
309
316
  // sprag harness init # write CLAUDE.md (5 sections) + ratchet.md
310
317
  // sprag harness uninit # remove harness block from CLAUDE.md (backup kept)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sprag-cli",
3
- "version": "3.58.1",
3
+ "version": "3.59.0",
4
4
  "description": "Lower your Claude Code bill without changing how you work: simple tasks go to cheaper models, documents go in as compact Markdown, and the statusline shows spend and savings live. Works with Codex too.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,9 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync, realpathSync } from 'node:fs';
2
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync, realpathSync, statSync } from 'node:fs';
3
3
  import { extname, join, resolve } from 'node:path';
4
4
  import { codexUserDir } from './agent.js';
5
5
  import { userDataDir } from './paths.js';
6
+ import { readResetMarks, isBeforeReset } from './saved-reset.cjs';
6
7
 
7
8
  const hash = (value) => createHash('sha256').update(value).digest('hex');
8
9
  const ledgerDir = ({ home = codexUserDir(), dir = userDataDir() } = {}) => join(dir, 'codex-doc2md', hash(resolve(home)));
@@ -26,13 +27,17 @@ export function recordCodexDocument(source, result, opts) {
26
27
  } catch { /* Accounting must not prevent document conversion. */ }
27
28
  }
28
29
 
30
+ // Records carry no timestamp field, so the reset mark is compared with the
31
+ // file's mtime. A source converted again rewrites its record and counts as new.
29
32
  export function codexDocumentTotals(opts) {
30
- const total = { docs: 0, byExt: [], scope: 'codex-total' };
33
+ const since = readResetMarks(opts?.dir ?? userDataDir())['codex-docs'];
34
+ const total = { docs: 0, byExt: [], scope: 'codex-total', since };
31
35
  const byExt = new Map();
32
36
  try {
33
37
  const dir = ledgerDir(opts);
34
38
  for (const file of readdirSync(dir).filter((name) => /^[a-f0-9]{64}\.json$/.test(name))) {
35
39
  try {
40
+ if (since !== null && isBeforeReset(statSync(join(dir, file)).mtimeMs, since)) continue;
36
41
  const event = JSON.parse(readFileSync(join(dir, file), 'utf8'));
37
42
  if (event?.version !== 1 || !['pdf', 'pptx', 'xlsx', 'xls', 'docx', 'fig'].includes(event.ext)) continue;
38
43
  total.docs++;
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import { randomBytes, randomUUID } from 'node:crypto';
4
4
  import { userDataDir } from './paths.js';
5
5
  import { roundUsd } from './savings-ledger.js';
6
+ import { readResetMarks, isBeforeReset } from './saved-reset.cjs';
6
7
  import { discoverCodexSessionFiles, parseCodexTurns } from './codex-parser.js';
7
8
  import { codexRunCost, codexPriceBook, readCodexPrices } from './codex-cache-policy.js';
8
9
 
@@ -262,10 +263,15 @@ export async function refreshCodexLedger({ dir = userDataDir(), home, now = Date
262
263
  return ledger;
263
264
  }
264
265
 
265
- /** Signed totals over priced runs; unpriced runs are counted, never summed as zero. */
266
+ /**
267
+ * Signed totals over priced runs; unpriced runs are counted, never summed as zero.
268
+ * Runs before the `codex-routing` reset mark are left out of every figure,
269
+ * `runs` and `priced` included; `since` reports the mark.
270
+ */
266
271
  export function codexRoutingSavedTotals({ dir = userDataDir(), now = Date.now() } = {}) {
267
- const events = Object.values(loadCodexLedger({ dir }).events);
268
- const totals = { week: 0, month: 0, total: 0, runs: events.length, priced: 0 };
272
+ const since = readResetMarks(dir)['codex-routing'];
273
+ const events = Object.values(loadCodexLedger({ dir }).events).filter((e) => !isBeforeReset(e.ts, since));
274
+ const totals = { week: 0, month: 0, total: 0, runs: events.length, priced: 0, since };
269
275
  for (const e of events) {
270
276
  if (!Number.isFinite(e.usd)) continue;
271
277
  totals.priced++;
@@ -23,6 +23,7 @@ Usage: sprag <command> --agent codex [options]
23
23
  brief [on|off|status] Current warnings or prompt-briefing preference
24
24
  last / history Context/rate-limit warnings and handoffs; --days, --session, --project
25
25
  handoff [--cwd path] Write Git state and Codex usage to HANDOFF-*.md
26
+ saved [reset|undo] Show, reset or restore the saved-money counters
26
27
  delegate on|off|status Native model routing and SubagentStart guidance
27
28
  delegate model <model> Default target for spawns without an explicit model
28
29
  delegate rules Codex-only category rules; add/rm with explicit scope
@@ -144,6 +145,7 @@ export async function run({ args, version }) {
144
145
  return;
145
146
  }
146
147
  if (cmd === 'delegate') return (await import('./codex-delegate.js')).run({ args, getArg, hasFlag, root });
148
+ if (cmd === 'saved') return (await import('./saved.js')).run({ args, hasFlag, agent: 'codex' });
147
149
  if (cmd === 'seed') return (await import('./seed.js')).run({ args, hasFlag, agent: 'codex', root });
148
150
  if (cmd === 'route-scan') {
149
151
  const rs = await import('../codex-route-scan.js');
@@ -56,9 +56,22 @@ export async function run({ args, hasFlag, numArg }) {
56
56
  // which model actually ran it.
57
57
  if (args[1] === 'savings') {
58
58
  const { loadLedger, delegationSavedTotals } = await import('../savings-ledger.js');
59
+ // Runs from before `sprag saved reset` are kept in the ledger but not listed.
60
+ const { readResetMarks, isBeforeReset } = await import('../saved-reset.cjs');
61
+ const { userDataDir } = await import('../paths.js');
62
+ const since = readResetMarks(userDataDir()).routing;
59
63
  const events = Object.entries(loadLedger().events)
60
64
  .map(([key, e]) => ({ key, ...e }))
65
+ .filter((e) => !isBeforeReset(e.ts, since))
61
66
  .sort((a, b) => b.ts - a.ts);
67
+ if (events.length === 0 && since !== null) {
68
+ // After a reset an empty list is the expected state, not a setup gap.
69
+ const at = new Date(since).toLocaleString(lang === 'ko' ? 'ko-KR' : 'en-US');
70
+ console.log(lang === 'ko'
71
+ ? `${at}에 카운터를 초기화한 뒤로 기록된 라우팅 절감이 아직 없습니다. 되돌리려면 \`sprag saved undo\` 를 실행하십시오.`
72
+ : `No routing savings recorded since the counter was reset at ${at}. Run \`sprag saved undo\` to restore it.`);
73
+ return;
74
+ }
62
75
  if (events.length === 0) {
63
76
  console.log(lang === 'ko'
64
77
  ? '기록된 라우팅 절감 없음. 승격된 룰이 실제로 위임을 일으킨 뒤 `route-scan --refresh` 를 돌리면 채워집니다.'
@@ -72,6 +85,12 @@ export async function run({ args, hasFlag, numArg }) {
72
85
  console.log(lang === 'ko'
73
86
  ? `🔀 라우팅 절감 누적 ${money(t.total)} (최근 7일 ${money(t.week)} · 30일 ${money(t.month)})`
74
87
  : `🔀 Routing saved, lifetime ${money(t.total)} (last 7d ${money(t.week)} · 30d ${money(t.month)})`);
88
+ if (since !== null) {
89
+ const at = new Date(since).toLocaleString(lang === 'ko' ? 'ko-KR' : 'en-US');
90
+ console.log(lang === 'ko'
91
+ ? ` ${at}에 카운터를 초기화했습니다. 이후 기록만 집계합니다.`
92
+ : ` Counter reset at ${at}; only later runs are counted.`);
93
+ }
75
94
 
76
95
  // Per model-pair rollup first: the "what moved where" question is what
77
96
  // this view exists to answer, and it is easier to read than the log.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Subcommand: saved — a stopwatch reset for the lifetime "saved" counters.
3
+ *
4
+ * sprag saved # totals since the last reset
5
+ * sprag saved reset [routing|doc2md|all] # start counting from now
6
+ * sprag saved undo # put the previous counters back
7
+ * (add --agent codex for the Codex counters: codex-routing, codex-docs)
8
+ *
9
+ * Nothing is deleted. A reset only records a timestamp, and the totals count
10
+ * events from that moment on; see src/saved-reset.cjs.
11
+ */
12
+
13
+ const SCOPE_SETS = {
14
+ claude: ['routing', 'doc2md'],
15
+ codex: ['codex-routing', 'codex-docs'],
16
+ };
17
+
18
+ /** Totals since each scope's mark: { usd } for money counters, { docs } for the Codex document counter. */
19
+ async function currentTotals(scope, dir) {
20
+ if (scope === 'routing') {
21
+ const { delegationSavedTotals } = await import('../savings-ledger.js');
22
+ return { usd: delegationSavedTotals(Date.now(), dir).total };
23
+ }
24
+ if (scope === 'doc2md') {
25
+ const { doc2mdSavedTotals } = await import('../doc2md-ledger.cjs');
26
+ return { usd: doc2mdSavedTotals(dir).total };
27
+ }
28
+ if (scope === 'codex-routing') {
29
+ const { codexRoutingSavedTotals } = await import('../codex-ledger.js');
30
+ return { usd: codexRoutingSavedTotals({ dir }).total };
31
+ }
32
+ const { codexDocumentTotals } = await import('../codex-doc2md-ledger.js');
33
+ return { docs: codexDocumentTotals({ dir }).docs };
34
+ }
35
+
36
+ export async function run({ args, hasFlag, agent = 'claude', dir: dirArg }) {
37
+ const { userLanguage } = await import('../config.js');
38
+ const { userDataDir } = await import('../paths.js');
39
+ const { readResetMarks, resetSaved, undoReset } = await import('../saved-reset.cjs');
40
+ const ko = userLanguage() === 'ko';
41
+ const dir = dirArg || userDataDir();
42
+ const scopes = SCOPE_SETS[agent === 'codex' ? 'codex' : 'claude'];
43
+ const suffix = agent === 'codex' ? ' --agent codex' : '';
44
+
45
+ const money = (v) => (v < 0 ? `-$${(-v).toFixed(2)}` : `$${v.toFixed(2)}`);
46
+ const fmtTotal = (t) => (t.docs !== undefined ? (ko ? `${t.docs}건` : `${t.docs} doc(s)`) : money(t.usd));
47
+ const fmtTime = (ms) => new Date(ms).toLocaleString(ko ? 'ko-KR' : 'en-US');
48
+ const label = (scope) => ({
49
+ routing: ko ? '라우팅 절감' : 'Routing saved',
50
+ doc2md: ko ? '문서 변환 절감' : 'Document conversion saved',
51
+ 'codex-routing': ko ? 'Codex 라우팅 절감' : 'Codex routing saved',
52
+ 'codex-docs': ko ? 'Codex 문서 변환' : 'Codex documents converted',
53
+ })[scope];
54
+
55
+ const sub = args[1];
56
+
57
+ if (sub === undefined) {
58
+ const marks = readResetMarks(dir);
59
+ for (const s of scopes) {
60
+ const t = await currentTotals(s, dir);
61
+ const since = marks[s] === null
62
+ ? (ko ? '초기화한 적 없음' : 'not reset')
63
+ : (ko ? `${fmtTime(marks[s])} 이후` : `since ${fmtTime(marks[s])}`);
64
+ console.log(`${label(s)}: ${fmtTotal(t)} (${since})`);
65
+ }
66
+ return;
67
+ }
68
+
69
+ if (sub === 'reset') {
70
+ const arg = args[2] || 'all';
71
+ const base = agent === 'codex' ? { routing: 'codex-routing', doc2md: 'codex-docs' } : { routing: 'routing', doc2md: 'doc2md' };
72
+ let targets;
73
+ if (arg === 'all') targets = scopes;
74
+ else if (scopes.includes(arg)) targets = [arg];
75
+ else if (base[arg]) targets = [base[arg]];
76
+ else {
77
+ console.error(ko
78
+ ? `알 수 없는 범위입니다: ${arg}. 사용할 수 있는 범위는 ${scopes.join(', ')}, all 입니다.`
79
+ : `Unknown scope: ${arg}. Valid scopes are ${scopes.join(', ')}, all.`);
80
+ process.exit(1);
81
+ }
82
+ const before = {};
83
+ for (const s of targets) before[s] = await currentTotals(s, dir);
84
+ resetSaved(targets, { dir });
85
+ for (const s of targets) {
86
+ console.log(ko
87
+ ? `${label(s)} 카운터를 0으로 초기화했습니다. 이전 합계는 ${fmtTotal(before[s])}입니다.`
88
+ : `Reset the ${label(s)} counter to zero. The previous total was ${fmtTotal(before[s])}.`);
89
+ }
90
+ console.log(ko
91
+ ? `원장 기록은 지우지 않았습니다. \`sprag saved undo${suffix}\`로 되돌릴 수 있습니다.`
92
+ : `No ledger data was deleted. Run \`sprag saved undo${suffix}\` to restore the previous counters.`);
93
+ return;
94
+ }
95
+
96
+ if (sub === 'undo') {
97
+ const undone = undoReset({ dir });
98
+ if (!undone) {
99
+ console.log(ko ? '되돌릴 초기화 기록이 없습니다.' : 'There is no reset to undo.');
100
+ return;
101
+ }
102
+ for (const s of undone.scopes) {
103
+ const t = await currentTotals(s, dir);
104
+ console.log(ko
105
+ ? `${label(s)} 카운터를 되돌렸습니다. 현재 합계는 ${fmtTotal(t)}입니다.`
106
+ : `Restored the ${label(s)} counter. The total is now ${fmtTotal(t)}.`);
107
+ }
108
+ return;
109
+ }
110
+
111
+ console.error(ko
112
+ ? `알 수 없는 하위 명령입니다: ${sub}. 사용법은 sprag saved [reset|undo] 입니다.`
113
+ : `Unknown subcommand: ${sub}. Usage: sprag saved [reset|undo].`);
114
+ process.exit(1);
115
+ }
@@ -69,6 +69,7 @@
69
69
 
70
70
  const fs = require('node:fs');
71
71
  const path = require('node:path');
72
+ const { readResetMarks, isBeforeReset } = require('./saved-reset.cjs');
72
73
 
73
74
  const WEEK_MS = 7 * 24 * 3600 * 1000;
74
75
  const MONTH_MS = 30 * 24 * 3600 * 1000;
@@ -185,13 +186,18 @@ function recordConversion(userDataDir, event) {
185
186
  * the lifetime rollup per format, priciest first, then most-converted. Never
186
187
  * throws; an unreadable ledger yields zeros, and the statusline hides the
187
188
  * chip on a zero.
189
+ *
190
+ * Events before the `doc2md` reset mark (`sprag saved reset`) are skipped in
191
+ * every figure, and `since` reports the mark (null when never reset).
188
192
  */
189
193
  function doc2mdSavedTotals(userDataDir, now = Date.now()) {
190
- const empty = () => ({ week: 0, month: 0, total: 0, docs: 0, tokens: 0, byExt: [] });
194
+ const since = readResetMarks(userDataDir).doc2md;
195
+ const empty = () => ({ week: 0, month: 0, total: 0, docs: 0, tokens: 0, byExt: [], since });
191
196
  const totals = empty();
192
197
  const byExt = new Map();
193
198
  try {
194
199
  for (const e of Object.values(loadLedger(userDataDir).events)) {
200
+ if (isBeforeReset(e.ts, since)) continue;
195
201
  const usd = Number(e.usd) || 0;
196
202
  totals.total += usd;
197
203
  totals.docs += 1;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * saved-reset — a stopwatch reset for the lifetime "saved" counters.
3
+ *
4
+ * The ledgers are append-only evidence, so a reset must not delete anything.
5
+ * Instead this file records one timestamp per counter ("mark"), and every
6
+ * totals function counts only events at or after its mark. Undo pops the
7
+ * latest reset and puts the previous marks back, so a reset is always
8
+ * reversible and the underlying ledger data is never touched.
9
+ *
10
+ * File: <userDataDir>/saved-reset.json
11
+ * { "version": 1,
12
+ * "marks": { "routing", "doc2md", "codex-routing", "codex-docs" }, // ms | null
13
+ * "history": [ { "at", "scopes": [...], "prev": { "<scope>": ms | null } } ] }
14
+ *
15
+ * CJS because doc2md-ledger.cjs reads it; ESM code imports it the same way.
16
+ * Every caller passes the data directory, so tests can point at a temp dir.
17
+ *
18
+ * Best-effort like every other state file here: a missing or unreadable file
19
+ * reads as "never reset", so a broken marker can only over-count, never hide
20
+ * events.
21
+ */
22
+
23
+ const fs = require('node:fs');
24
+ const path = require('node:path');
25
+
26
+ const VERSION = 1;
27
+ const SCOPES = ['routing', 'doc2md', 'codex-routing', 'codex-docs'];
28
+ /** Cap on remembered resets, so the file cannot grow without bound. */
29
+ const MAX_HISTORY = 50;
30
+
31
+ const emptyMarks = () => Object.fromEntries(SCOPES.map((s) => [s, null]));
32
+
33
+ function resetPath(dir) {
34
+ return path.join(dir, 'saved-reset.json');
35
+ }
36
+
37
+ const markOrNull = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
38
+
39
+ function readState(dir) {
40
+ try {
41
+ const data = JSON.parse(fs.readFileSync(resetPath(dir), 'utf8'));
42
+ if (!data || typeof data !== 'object' || data.version !== VERSION) return { marks: emptyMarks(), history: [] };
43
+ const marks = emptyMarks();
44
+ for (const s of SCOPES) marks[s] = markOrNull(data.marks && data.marks[s]);
45
+ const history = Array.isArray(data.history)
46
+ ? data.history.filter((h) => h && Array.isArray(h.scopes) && h.prev && typeof h.prev === 'object')
47
+ : [];
48
+ return { marks, history };
49
+ } catch {
50
+ return { marks: emptyMarks(), history: [] };
51
+ }
52
+ }
53
+
54
+ /** Atomic write: a temp file renamed over the target, so a reader never sees half a file. */
55
+ function writeState(dir, state) {
56
+ const file = resetPath(dir);
57
+ fs.mkdirSync(dir, { recursive: true });
58
+ const tmp = `${file}.${process.pid}.tmp`;
59
+ const text = JSON.stringify({ version: VERSION, marks: state.marks, history: state.history }) + '\n';
60
+ try {
61
+ fs.writeFileSync(tmp, text);
62
+ fs.renameSync(tmp, file);
63
+ } catch {
64
+ try { fs.rmSync(tmp, { force: true }); } catch { /* nothing to clean */ }
65
+ fs.writeFileSync(file, text);
66
+ }
67
+ }
68
+
69
+ /** Current marks. Never throws; a missing or corrupt file means no reset. */
70
+ function readResetMarks(dir) {
71
+ return readState(dir).marks;
72
+ }
73
+
74
+ /**
75
+ * Reset the given scopes to `now`. Returns the new marks. Unknown scopes are
76
+ * ignored; the caller validates user input.
77
+ */
78
+ function resetSaved(scopes, { dir, now = Date.now() } = {}) {
79
+ const state = readState(dir);
80
+ const valid = [...new Set(scopes)].filter((s) => SCOPES.includes(s));
81
+ if (valid.length === 0) return state.marks;
82
+ const prev = {};
83
+ for (const s of valid) {
84
+ prev[s] = state.marks[s];
85
+ state.marks[s] = now;
86
+ }
87
+ state.history.push({ at: now, scopes: valid, prev });
88
+ if (state.history.length > MAX_HISTORY) state.history.splice(0, state.history.length - MAX_HISTORY);
89
+ writeState(dir, state);
90
+ return state.marks;
91
+ }
92
+
93
+ /**
94
+ * Undo the latest reset. Returns { scopes, marks } with the restored marks,
95
+ * or null when there is nothing to undo.
96
+ */
97
+ function undoReset({ dir } = {}) {
98
+ const state = readState(dir);
99
+ const last = state.history.pop();
100
+ if (!last) return null;
101
+ for (const s of last.scopes) {
102
+ if (SCOPES.includes(s)) state.marks[s] = markOrNull(last.prev[s]);
103
+ }
104
+ writeState(dir, state);
105
+ return { scopes: last.scopes, marks: state.marks };
106
+ }
107
+
108
+ /**
109
+ * True when an event at `ts` is excluded by `mark`: a mark exists and the
110
+ * event is older than it. An event with no usable timestamp cannot be shown
111
+ * to postdate the reset, so it is excluded too.
112
+ */
113
+ function isBeforeReset(ts, mark) {
114
+ if (mark === null || mark === undefined) return false;
115
+ return !Number.isFinite(ts) || ts < mark;
116
+ }
117
+
118
+ module.exports = { SCOPES, resetPath, readResetMarks, resetSaved, undoReset, isBeforeReset };
@@ -26,6 +26,7 @@ import { readFileSync } from 'node:fs';
26
26
  import { join } from 'node:path';
27
27
  import { userDataDir } from './paths.js';
28
28
  import { writeStateFile } from './state-file.js';
29
+ import { readResetMarks, isBeforeReset } from './saved-reset.cjs';
29
30
 
30
31
  const WEEK_MS = 7 * 24 * 3600 * 1000;
31
32
  const MONTH_MS = 30 * 24 * 3600 * 1000;
@@ -140,13 +141,19 @@ export function modelFamily(model) {
140
141
  * Rolling totals: last 7 days, last 30 days, and lifetime, plus `pairs` — the
141
142
  * lifetime rollup by family-level model change, priciest first. `now` is
142
143
  * injectable for tests. Never throws — an unreadable ledger yields zeros.
144
+ *
145
+ * Events before the `routing` reset mark (`sprag saved reset`) are skipped
146
+ * everywhere, and `since` reports the mark (null when never reset). `dir` is
147
+ * the user data directory the mark is read from.
143
148
  */
144
- export function delegationSavedTotals(now = Date.now()) {
145
- const empty = () => ({ week: 0, month: 0, total: 0, pairs: [] });
149
+ export function delegationSavedTotals(now = Date.now(), dir = userDataDir()) {
150
+ const since = readResetMarks(dir).routing;
151
+ const empty = () => ({ week: 0, month: 0, total: 0, pairs: [], since });
146
152
  const totals = empty();
147
153
  const byPair = new Map();
148
154
  try {
149
155
  for (const e of Object.values(loadLedger().events)) {
156
+ if (isBeforeReset(e.ts, since)) continue;
150
157
  const usd = Number(e.usd);
151
158
  if (!Number.isFinite(usd)) continue;
152
159
  totals.total += usd;