routstrd 0.3.3 → 0.3.5

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/dist/index.js CHANGED
@@ -13876,9 +13876,6 @@ var init_constants = __esm(() => {
13876
13876
  // src/tui/usage/data.ts
13877
13877
  async function fetchStatus() {
13878
13878
  try {
13879
- const running = await isDaemonRunning();
13880
- if (!running)
13881
- return null;
13882
13879
  const result = await callDaemon("/status");
13883
13880
  if (result.error)
13884
13881
  return null;
@@ -13895,9 +13892,6 @@ async function fetchStatus() {
13895
13892
  }
13896
13893
  async function fetchBalance() {
13897
13894
  try {
13898
- const running = await isDaemonRunning();
13899
- if (!running)
13900
- return null;
13901
13895
  const result = await callDaemon("/keys/balance");
13902
13896
  if (result.error)
13903
13897
  return null;
@@ -13914,9 +13908,6 @@ async function fetchBalance() {
13914
13908
  }
13915
13909
  async function fetchUsageSummary() {
13916
13910
  try {
13917
- const running = await isDaemonRunning();
13918
- if (!running)
13919
- return null;
13920
13911
  const tz = new Date().getTimezoneOffset();
13921
13912
  const result = await callDaemon(`/usage/summary?tz=${tz}`);
13922
13913
  if (result.error)
@@ -13950,9 +13941,6 @@ function formatNumber(n) {
13950
13941
  }
13951
13942
  async function fetchClients() {
13952
13943
  try {
13953
- const running = await isDaemonRunning();
13954
- if (!running)
13955
- return [];
13956
13944
  const result = await callDaemon("/clients");
13957
13945
  if (result.error)
13958
13946
  return [];
@@ -14305,13 +14293,22 @@ function renderToday(stats, width) {
14305
14293
  const currentHour = new Date().getHours();
14306
14294
  const _now = new Date;
14307
14295
  const todayDateStr = `${_now.getFullYear()}-${String(_now.getMonth() + 1).padStart(2, "0")}-${String(_now.getDate()).padStart(2, "0")}`;
14308
- let todayStats;
14309
- let recentDays;
14310
- let hourlyMap;
14296
+ let todayStats = {
14297
+ date: todayDateStr,
14298
+ requests: 0,
14299
+ satsCost: 0,
14300
+ promptTokens: 0,
14301
+ completionTokens: 0,
14302
+ totalTokens: 0
14303
+ };
14304
+ let recentDays = [];
14305
+ let hourlyMap = new Map;
14311
14306
  if (stats.summary) {
14312
14307
  const { days, hoursToday } = stats.summary;
14313
14308
  const todayDayStat = days[0]?.date === todayDateStr ? days[0] : undefined;
14314
- todayStats = todayDayStat ? { date: todayDayStat.date, requests: todayDayStat.requests, satsCost: todayDayStat.satsCost, promptTokens: todayDayStat.promptTokens, completionTokens: todayDayStat.completionTokens, totalTokens: todayDayStat.totalTokens } : { date: todayDateStr, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
14309
+ if (todayDayStat) {
14310
+ todayStats = { date: todayDayStat.date, requests: todayDayStat.requests, satsCost: todayDayStat.satsCost, promptTokens: todayDayStat.promptTokens, completionTokens: todayDayStat.completionTokens, totalTokens: todayDayStat.totalTokens };
14311
+ }
14315
14312
  recentDays = days.filter((d) => d.date !== todayDateStr).slice(0, 6);
14316
14313
  hourlyMap = new Map(hoursToday.map((h) => [h.hour, { requests: h.requests, satsCost: h.satsCost }]));
14317
14314
  }
@@ -14606,10 +14603,9 @@ async function runUsageTui() {
14606
14603
  let clients = [];
14607
14604
  let visibleTabs = getVisibleTabs(false);
14608
14605
  let refreshInterval = null;
14609
- let shouldUpdate = true;
14610
14606
  let autoRefresh = true;
14611
14607
  let cleanedUp = false;
14612
- let rendering = false;
14608
+ let fetching = false;
14613
14609
  if (isInteractive) {
14614
14610
  stdout.write(enterAlternateScreen() + hideCursor());
14615
14611
  stdin.setRawMode?.(true);
@@ -14633,55 +14629,70 @@ async function runUsageTui() {
14633
14629
  }
14634
14630
  process.on("SIGINT", () => cleanup(0));
14635
14631
  process.on("SIGTERM", () => cleanup(0));
14636
- async function render2(forceFetch = false) {
14637
- if (rendering)
14632
+ async function fetchData() {
14633
+ if (fetching)
14638
14634
  return;
14639
- rendering = true;
14635
+ fetching = true;
14640
14636
  try {
14641
- const width = getWidth();
14642
- const height = getHeight();
14643
- if (forceFetch || shouldUpdate) {
14644
- stats = await fetchUsageSummary();
14645
- balance = await fetchBalance();
14646
- status = await fetchStatus();
14647
- clients = await fetchClients();
14637
+ const running2 = await isDaemonRunning();
14638
+ if (!running2) {
14639
+ stats = null;
14640
+ } else {
14641
+ const [newStats, newBalance, newStatus, newClients] = await Promise.all([
14642
+ fetchUsageSummary(),
14643
+ fetchBalance(),
14644
+ fetchStatus(),
14645
+ fetchClients()
14646
+ ]);
14647
+ if (newStats)
14648
+ stats = newStats;
14649
+ if (newBalance)
14650
+ balance = newBalance;
14651
+ if (newStatus)
14652
+ status = newStatus;
14653
+ if (newClients && newClients.length > 0)
14654
+ clients = newClients;
14648
14655
  const npubsVisible = hasAnyNpubs(clients);
14649
14656
  visibleTabs = getVisibleTabs(npubsVisible);
14650
14657
  if (currentTab === "npubs" && !npubsVisible) {
14651
14658
  currentTab = "clients";
14652
14659
  vimState.scrollPos = 0;
14653
14660
  }
14654
- shouldUpdate = false;
14655
14661
  }
14656
- if (!stats) {
14657
- stdout.write(moveCursor(1, 1) + eraseDown() + `${COLORS.red}Error: Could not fetch usage data.${COLORS.reset}
14662
+ render2();
14663
+ } finally {
14664
+ fetching = false;
14665
+ }
14666
+ }
14667
+ function render2() {
14668
+ const width = getWidth();
14669
+ const height = getHeight();
14670
+ if (!stats) {
14671
+ stdout.write(moveCursor(1, 1) + eraseDown() + `${COLORS.red}Error: Could not fetch usage data.${COLORS.reset}
14658
14672
  ` + `Make sure routstrd is running.
14659
14673
  ` + `
14660
14674
  Press Q to quit.`);
14661
- return;
14662
- }
14663
- const content = renderTabContent(currentTab, stats, balance, status, width, clients);
14664
- const footer = `${COLORS.dim}Press [Q] to quit, [R] to refresh, [A] to toggle auto-refresh${autoRefresh ? " (on)" : " (off)"} scroll:${vimState.scrollPos}${COLORS.reset}${vimState.mode === "normal" ? ` ${COLORS.yellow}vim: hjkl/arrows, / search, g top, gg bottom${COLORS.reset}` : ""}`;
14665
- const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar();
14666
- const chromeLines = chrome.split(`
14675
+ return;
14676
+ }
14677
+ const content = renderTabContent(currentTab, stats, balance, status, width, clients);
14678
+ const footer = `${COLORS.dim}Press [Q] to quit, [R] to refresh, [A] to toggle auto-refresh${autoRefresh ? " (on)" : " (off)"} scroll:${vimState.scrollPos}${COLORS.reset}${vimState.mode === "normal" ? ` ${COLORS.yellow}vim: hjkl/arrows, / search, g top, gg bottom${COLORS.reset}` : ""}`;
14679
+ const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar();
14680
+ const chromeLines = chrome.split(`
14667
14681
  `).length - 1;
14668
- const footerSeparator = renderSeparator(width);
14669
- const footerLines = footerSeparator.split(`
14682
+ const footerSeparator = renderSeparator(width);
14683
+ const footerLines = footerSeparator.split(`
14670
14684
  `).length - 1;
14671
- const contentViewportHeight = Math.max(1, height - chromeLines - footerLines - 1);
14672
- const visibleContent = applyScrollToContent(content, contentViewportHeight);
14673
- const footerBlock = (visibleContent ? `
14685
+ const contentViewportHeight = Math.max(1, height - chromeLines - footerLines - 1);
14686
+ const visibleContent = applyScrollToContent(content, contentViewportHeight);
14687
+ const footerBlock = (visibleContent ? `
14674
14688
  ` : "") + footerSeparator + footer;
14675
- stdout.write(moveCursor(1, 1) + eraseDown() + chrome + visibleContent + footerBlock);
14676
- } finally {
14677
- rendering = false;
14678
- }
14689
+ stdout.write(moveCursor(1, 1) + eraseDown() + chrome + visibleContent + footerBlock);
14679
14690
  }
14680
14691
  const handleKey = (key) => {
14681
14692
  if (vimState.isSearching) {
14682
14693
  if (key === "\x1B" || key === "\x1B[3~") {
14683
14694
  exitSearchMode();
14684
- render2(false);
14695
+ render2();
14685
14696
  return;
14686
14697
  }
14687
14698
  if (key === "\r" || key === `
@@ -14689,117 +14700,115 @@ Press Q to quit.`);
14689
14700
  if (stats?.entries)
14690
14701
  performSearch(vimState.searchQuery, stats.entries);
14691
14702
  exitSearchMode();
14692
- render2(false);
14703
+ render2();
14693
14704
  return;
14694
14705
  }
14695
14706
  if (key === "\x7F" || key === "\b") {
14696
14707
  vimState.searchQuery = vimState.searchQuery.slice(0, -1);
14697
14708
  if (stats?.entries)
14698
14709
  performSearch(vimState.searchQuery, stats.entries);
14699
- render2(false);
14710
+ render2();
14700
14711
  return;
14701
14712
  }
14702
14713
  if (key === "\x03") {
14703
14714
  exitSearchMode();
14704
- render2(false);
14715
+ render2();
14705
14716
  return;
14706
14717
  }
14707
14718
  if (key.length === 1 && key.charCodeAt(0) >= 32 && key.charCodeAt(0) < 127) {
14708
14719
  vimState.searchQuery += key;
14709
14720
  if (stats?.entries)
14710
14721
  performSearch(vimState.searchQuery, stats.entries);
14711
- render2(false);
14722
+ render2();
14712
14723
  }
14713
14724
  return;
14714
14725
  }
14715
14726
  if (key === "q" || key === "Q" || key === "\x03")
14716
14727
  return cleanup(0);
14717
14728
  if (key === "r" || key === "R") {
14718
- shouldUpdate = true;
14719
- render2(true);
14729
+ fetchData();
14720
14730
  return;
14721
14731
  }
14722
14732
  if (key === "a" || key === "A") {
14723
14733
  autoRefresh = !autoRefresh;
14724
- shouldUpdate = true;
14725
- render2(false);
14734
+ render2();
14726
14735
  return;
14727
14736
  }
14728
14737
  if (key === "j" || key === "\x1B[B" || key === "\x1BOB") {
14729
14738
  scrollDown();
14730
- render2(false);
14739
+ render2();
14731
14740
  return;
14732
14741
  }
14733
14742
  if (key === "k" || key === "\x1B[A" || key === "\x1BOA") {
14734
14743
  scrollUp();
14735
- render2(false);
14744
+ render2();
14736
14745
  return;
14737
14746
  }
14738
14747
  if (key === "l" || key === "\x1B[C" || key === "\x1BOC") {
14739
14748
  const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
14740
14749
  currentTab = visibleTabs[(currentIdx + 1) % visibleTabs.length].id;
14741
14750
  vimState.scrollPos = 0;
14742
- render2(false);
14751
+ render2();
14743
14752
  return;
14744
14753
  }
14745
14754
  if (key === "h" || key === "\x1B[D" || key === "\x1BOD") {
14746
14755
  const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
14747
14756
  currentTab = visibleTabs[(currentIdx - 1 + visibleTabs.length) % visibleTabs.length].id;
14748
14757
  vimState.scrollPos = 0;
14749
- render2(false);
14758
+ render2();
14750
14759
  return;
14751
14760
  }
14752
14761
  if (key === "g") {
14753
14762
  if (vimState.lastKey === "g" && Date.now() - vimState.lastKeyTime < 300) {
14754
14763
  scrollToBottom();
14755
14764
  vimState.lastKey = "";
14756
- render2(false);
14765
+ render2();
14757
14766
  return;
14758
14767
  }
14759
14768
  vimState.lastKey = "g";
14760
14769
  vimState.lastKeyTime = Date.now();
14761
14770
  scrollToTop();
14762
- render2(false);
14771
+ render2();
14763
14772
  return;
14764
14773
  }
14765
14774
  if (key === "\x02") {
14766
14775
  pageUp();
14767
- render2(false);
14776
+ render2();
14768
14777
  return;
14769
14778
  }
14770
14779
  if (key === "\x06") {
14771
14780
  pageDown();
14772
- render2(false);
14781
+ render2();
14773
14782
  return;
14774
14783
  }
14775
14784
  if (key === "\x15") {
14776
14785
  scrollUp(10);
14777
- render2(false);
14786
+ render2();
14778
14787
  return;
14779
14788
  }
14780
14789
  if (key === "\x04") {
14781
14790
  scrollDown(10);
14782
- render2(false);
14791
+ render2();
14783
14792
  return;
14784
14793
  }
14785
14794
  if (key === "\x1B[H" || key === "\x1B[1~" || key === "\x1BOH") {
14786
14795
  scrollToTop();
14787
- render2(false);
14796
+ render2();
14788
14797
  return;
14789
14798
  }
14790
14799
  if (key === "\x1B[F" || key === "\x1B[4~" || key === "\x1BOF") {
14791
14800
  scrollToBottom();
14792
- render2(false);
14801
+ render2();
14793
14802
  return;
14794
14803
  }
14795
14804
  if (key === "/") {
14796
14805
  startSearch(false);
14797
- render2(false);
14806
+ render2();
14798
14807
  return;
14799
14808
  }
14800
14809
  if (key === "?") {
14801
14810
  startSearch(true);
14802
- render2(false);
14811
+ render2();
14803
14812
  return;
14804
14813
  }
14805
14814
  if (key === "n") {
@@ -14807,7 +14816,7 @@ Press Q to quit.`);
14807
14816
  prevSearchResult(stats?.entries.length || 0);
14808
14817
  else
14809
14818
  nextSearchResult(stats?.entries.length || 0);
14810
- render2(false);
14819
+ render2();
14811
14820
  return;
14812
14821
  }
14813
14822
  if (key === "N") {
@@ -14815,28 +14824,27 @@ Press Q to quit.`);
14815
14824
  nextSearchResult(stats?.entries.length || 0);
14816
14825
  else
14817
14826
  prevSearchResult(stats?.entries.length || 0);
14818
- render2(false);
14827
+ render2();
14819
14828
  return;
14820
14829
  }
14821
14830
  if (key === "\x1B") {
14822
14831
  scrollToTop();
14823
- render2(false);
14832
+ render2();
14824
14833
  return;
14825
14834
  }
14826
14835
  const tab = visibleTabs.find((t) => t.key === key);
14827
14836
  if (tab) {
14828
14837
  currentTab = tab.id;
14829
14838
  vimState.scrollPos = 0;
14830
- render2(false);
14839
+ render2();
14831
14840
  }
14832
14841
  };
14833
14842
  if (isInteractive)
14834
14843
  stdin.on("data", handleKey);
14835
- await render2(true);
14844
+ await fetchData();
14836
14845
  refreshInterval = setInterval(() => {
14837
14846
  if (autoRefresh) {
14838
- shouldUpdate = true;
14839
- render2(true);
14847
+ fetchData();
14840
14848
  }
14841
14849
  }, 2000);
14842
14850
  }
@@ -15588,7 +15596,7 @@ async function isCocodInstalled(cocodPath) {
15588
15596
  // package.json
15589
15597
  var package_default = {
15590
15598
  name: "routstrd",
15591
- version: "0.3.3",
15599
+ version: "0.3.5",
15592
15600
  module: "src/index.ts",
15593
15601
  type: "module",
15594
15602
  private: false,
@@ -15612,7 +15620,7 @@ var package_default = {
15612
15620
  },
15613
15621
  dependencies: {
15614
15622
  "@cashu/cashu-ts": "^4.3.0",
15615
- "@routstr/sdk": "^0.3.11",
15623
+ "@routstr/sdk": "^0.3.14",
15616
15624
  "applesauce-core": "^5.1.0",
15617
15625
  "applesauce-relay": "^5.1.0",
15618
15626
  "applesauce-wallet-connect": "^6.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routstrd",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
6
  "private": false,
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@cashu/cashu-ts": "^4.3.0",
27
- "@routstr/sdk": "^0.3.11",
27
+ "@routstr/sdk": "^0.3.14",
28
28
  "applesauce-core": "^5.1.0",
29
29
  "applesauce-relay": "^5.1.0",
30
30
  "applesauce-wallet-connect": "^6.0.0",
@@ -6,15 +6,27 @@ import { getUsageSummary, __resetUsageSummaryCacheForTest } from "./usage-summar
6
6
 
7
7
  // ─── Test fixtures ────────────────────────────────────────────────────────────
8
8
  //
9
- // All timestamps fall within 30 days of 2026-06-02 (today as of this writing).
9
+ // All timestamps are computed relative to "now" so the fixtures never age
10
+ // out of getUsageSummary's 30-day rolling window. We anchor to a recent UTC
11
+ // midnight ~10 days ago and keep the same hour-of-day structure the
12
+ // tz-bucketing assertions depend on.
13
+ //
10
14
  // tz = 300 (UTC-5 / EST). After shifting by -300min, UTC dates become:
11
- // d1: 2026-05-20T10:00Z → shifted 2026-05-20T05:00Z → local day "2026-05-20", hour 05
12
- // d2: 2026-05-21T03:00Z → shifted 2026-05-20T22:00Z → local day "2026-05-20", hour 22
13
- // d3: 2026-05-21T06:00Z → shifted 2026-05-21T01:00Z → local day "2026-05-21", hour 01
14
-
15
- const D1 = new Date("2026-05-20T10:00:00Z").getTime(); // local 2026-05-20, hour 05
16
- const D2 = new Date("2026-05-21T03:00:00Z").getTime(); // local 2026-05-20, hour 22
17
- const D3 = new Date("2026-05-21T06:00:00Z").getTime(); // local 2026-05-21, hour 01
15
+ // d1: <anchor> 10:00Z → local <anchor> 05:00 → local day "<anchor>"
16
+ // d2: <anchor+1d> 03:00Z → local <anchor> 22:00 → local day "<anchor>"
17
+ // d3: <anchor+1d> 06:00Z → local <anchor+1d> 01:00 → local day "<anchor+1d>"
18
+
19
+ const _BASE_UTC_MIDNIGHT = Math.floor(Date.now() / 86_400_000) * 86_400_000;
20
+ const _ANCHOR_MS = _BASE_UTC_MIDNIGHT - 10 * 86_400_000; // 10 days ago UTC
21
+ const D1 = _ANCHOR_MS + 10 * 3_600_000; // 10:00Z → local day <anchor>
22
+ const D2 = _ANCHOR_MS + 1 * 86_400_000 + 3 * 3_600_000; // 03:00Z next day → local day <anchor>
23
+ const D3 = _ANCHOR_MS + 1 * 86_400_000 + 6 * 3_600_000; // 06:00Z next day → local day <anchor+1d>
24
+
25
+ /** Compute the local-day date string the same way the SDK's day grouping does. */
26
+ function localDayDateStr(ts: number, tzOffsetMinutes: number): string {
27
+ const d = new Date(ts - tzOffsetMinutes * 60_000);
28
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
29
+ }
18
30
 
19
31
  const BASE_ENTRY: Omit<UsageTrackingEntry, "id" | "timestamp" | "modelId" | "client" | "totalTokens"> = {
20
32
  baseUrl: "https://api.openai.com/",
@@ -152,23 +164,24 @@ describe("getUsageSummary", () => {
152
164
  const summary = await getUsageSummary(driver, CLIENTS, TZ);
153
165
  const { days } = summary;
154
166
 
155
- // D1 (10:00Z) → local 2026-05-20, D2 (03:00Z) → local 2026-05-20, D3 (06:00Z) → local 2026-05-21
156
- // So: day 2026-05-20 has e1+e2, day 2026-05-21 has e3+e4
167
+ // D1 (10:00Z) and D2 (next day 03:00Z) both bucket to local day <anchor>;
168
+ // D3 (next day 06:00Z) buckets to local day <anchor+1d>.
169
+ // So: day <anchor> has e1+e2, day <anchor+1d> has e3+e4
157
170
  expect(days.length).toBeGreaterThanOrEqual(2);
158
171
 
159
- // Most-recent-first: 2026-05-21 first
160
- expect(days[0]!.date).toBe("2026-05-21");
172
+ // Most-recent-first: <anchor+1d> first
173
+ expect(days[0]!.date).toBe(localDayDateStr(D3, TZ));
161
174
  expect(days[0]!.requests).toBe(2); // e3, e4
162
175
  expect(days[0]!.satsCost).toBe(8 + 50);
163
176
 
164
- expect(days[1]!.date).toBe("2026-05-20");
177
+ expect(days[1]!.date).toBe(localDayDateStr(D1, TZ));
165
178
  expect(days[1]!.requests).toBe(2); // e1, e2
166
179
  expect(days[1]!.satsCost).toBe(10 + 20);
167
180
  });
168
181
 
169
182
  it("returns empty hoursToday (test entries are not today)", async () => {
170
183
  const summary = await getUsageSummary(driver, CLIENTS, TZ);
171
- // Test entries are in May 2026, not today (June 2026)
184
+ // Test entries are ~10 days ago, not today
172
185
  expect(summary.hoursToday).toHaveLength(0);
173
186
  });
174
187