routstrd 0.2.17 → 0.2.19
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/bun.lock +2 -2
- package/dist/daemon/index.js +190 -124
- package/dist/index.js +193 -58
- package/package.json +2 -2
- package/src/start-daemon.ts +1 -16
- package/src/tui/usage/app.ts +20 -10
- package/src/tui/usage/constants.ts +15 -2
- package/src/tui/usage/data.ts +75 -1
- package/src/tui/usage/render.ts +110 -12
- package/src/tui/usage/types.ts +10 -1
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import type { Tab } from "./types.ts";
|
|
2
2
|
|
|
3
|
-
export const
|
|
3
|
+
export const ALL_TABS: Tab[] = [
|
|
4
4
|
{ id: "overview", name: "Overview", key: "1" },
|
|
5
5
|
{ id: "today", name: "Today", key: "2" },
|
|
6
6
|
{ id: "models", name: "Models", key: "3" },
|
|
7
7
|
{ id: "providers", name: "Providers", key: "4" },
|
|
8
8
|
{ id: "tokens", name: "Tokens", key: "5" },
|
|
9
9
|
{ id: "clients", name: "Clients", key: "6" },
|
|
10
|
-
{ id: "
|
|
10
|
+
{ id: "npubs", name: "Npubs", key: "7" },
|
|
11
|
+
{ id: "recent", name: "Recent", key: "8" },
|
|
11
12
|
];
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Returns the visible tab list, hiding the Npubs tab when no clients
|
|
16
|
+
* have an ownerNpub. Keys are re-assigned sequentially (1..N).
|
|
17
|
+
*/
|
|
18
|
+
export function getVisibleTabs(hasNpubs: boolean): Tab[] {
|
|
19
|
+
const tabs = ALL_TABS.filter((t) => t.id !== "npubs" || hasNpubs);
|
|
20
|
+
return tabs.map((t, i) => ({ ...t, key: String(i + 1) }));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Default tab list (no npub data yet). */
|
|
24
|
+
export const TABS: Tab[] = getVisibleTabs(false);
|
|
25
|
+
|
|
13
26
|
export const COLORS = {
|
|
14
27
|
reset: "\x1b[0m",
|
|
15
28
|
bold: "\x1b[1m",
|
package/src/tui/usage/data.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { UsageTrackingEntry } from "../../daemon/types.ts";
|
|
2
2
|
import { callDaemon, isDaemonRunning } from "../../utils/daemon-client.ts";
|
|
3
|
-
import type { ClientStats, DayStats, ModelStats, ProviderStats, UsageStats } from "./types.ts";
|
|
3
|
+
import type { ClientStats, DayStats, ModelStats, NpubStats, ProviderStats, UsageStats } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export { isDaemonRunning };
|
|
4
6
|
|
|
5
7
|
export interface BalanceKey {
|
|
6
8
|
id: string;
|
|
@@ -223,6 +225,78 @@ export function getClientStats(entries: UsageTrackingEntry[]): ClientStats[] {
|
|
|
223
225
|
return Array.from(clients.values()).sort((a, b) => b.satsCost - a.satsCost);
|
|
224
226
|
}
|
|
225
227
|
|
|
228
|
+
// ─── Client / Npub helpers ────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
export interface ClientInfo {
|
|
231
|
+
clientId: string;
|
|
232
|
+
name: string;
|
|
233
|
+
ownerNpub?: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function fetchClients(): Promise<ClientInfo[]> {
|
|
237
|
+
try {
|
|
238
|
+
const running = await isDaemonRunning();
|
|
239
|
+
if (!running) return [];
|
|
240
|
+
|
|
241
|
+
const result = await callDaemon("/clients");
|
|
242
|
+
if (result.error) return [];
|
|
243
|
+
|
|
244
|
+
const output = result.output as {
|
|
245
|
+
clients?: Array<{ id: string; name: string; ownerNpub?: string }>;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
return (output?.clients || []).map((c) => ({
|
|
249
|
+
clientId: c.id,
|
|
250
|
+
name: c.name,
|
|
251
|
+
ownerNpub: c.ownerNpub,
|
|
252
|
+
}));
|
|
253
|
+
} catch {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function hasAnyNpubs(clients: ClientInfo[]): boolean {
|
|
259
|
+
return clients.some((c) => !!c.ownerNpub);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function getNpubStats(entries: UsageTrackingEntry[], clients: ClientInfo[]): NpubStats[] {
|
|
263
|
+
// Build client-id → ownerNpub lookup
|
|
264
|
+
const clientNpubMap = new Map<string, string>();
|
|
265
|
+
for (const c of clients) {
|
|
266
|
+
if (c.ownerNpub) {
|
|
267
|
+
clientNpubMap.set(c.clientId, c.ownerNpub);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Aggregate usage per npub
|
|
272
|
+
const npubs = new Map<string, NpubStats>();
|
|
273
|
+
for (const entry of entries) {
|
|
274
|
+
const npub = clientNpubMap.get(entry.client || "");
|
|
275
|
+
if (!npub) continue; // skip entries whose client has no ownerNpub
|
|
276
|
+
|
|
277
|
+
const existing = npubs.get(npub) || {
|
|
278
|
+
npub,
|
|
279
|
+
requests: 0,
|
|
280
|
+
satsCost: 0,
|
|
281
|
+
promptTokens: 0,
|
|
282
|
+
completionTokens: 0,
|
|
283
|
+
totalTokens: 0,
|
|
284
|
+
};
|
|
285
|
+
npubs.set(npub, {
|
|
286
|
+
...existing,
|
|
287
|
+
requests: existing.requests + 1,
|
|
288
|
+
satsCost: existing.satsCost + entry.satsCost,
|
|
289
|
+
promptTokens: existing.promptTokens + entry.promptTokens,
|
|
290
|
+
completionTokens: existing.completionTokens + entry.completionTokens,
|
|
291
|
+
totalTokens: existing.totalTokens + entry.totalTokens,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return Array.from(npubs.values()).sort((a, b) => b.satsCost - a.satsCost);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ─── Totals ───────────────────────────────────────────────────────────
|
|
299
|
+
|
|
226
300
|
export function getTotals(entries: UsageTrackingEntry[] | undefined) {
|
|
227
301
|
if (!entries || !Array.isArray(entries)) {
|
|
228
302
|
return { promptTokens: 0, completionTokens: 0, totalTokens: 0, satsCost: 0 };
|
package/src/tui/usage/render.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CLIENT_COLORS, COLORS, MODEL_COLORS
|
|
1
|
+
import { CLIENT_COLORS, COLORS, MODEL_COLORS } from "./constants.ts";
|
|
2
|
+
import type { Tab } from "./types.ts";
|
|
2
3
|
import {
|
|
3
4
|
formatDate,
|
|
4
5
|
formatNumber,
|
|
@@ -7,9 +8,11 @@ import {
|
|
|
7
8
|
getDayStats,
|
|
8
9
|
getHourlyToday,
|
|
9
10
|
getModelStats,
|
|
11
|
+
getNpubStats,
|
|
10
12
|
getProviderStats,
|
|
11
13
|
getTodayStart,
|
|
12
14
|
getTotals,
|
|
15
|
+
type ClientInfo,
|
|
13
16
|
} from "./data.ts";
|
|
14
17
|
import { vimState } from "./state.ts";
|
|
15
18
|
import { stripAnsi } from "./terminal.ts";
|
|
@@ -30,10 +33,11 @@ function formatReqs(value: number): string {
|
|
|
30
33
|
return value.toString();
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
export function renderHeader(activeTab: TabId, width: number): string {
|
|
36
|
+
export function renderHeader(activeTab: TabId, width: number, visibleTabs: Tab[]): string {
|
|
34
37
|
const title = `${COLORS.bold}${COLORS.cyan}ROUTSTRD USAGE MONITOR${COLORS.reset}`;
|
|
35
38
|
const vimIndicator = `${COLORS.yellow}[vim]${COLORS.reset}`;
|
|
36
|
-
const
|
|
39
|
+
const maxKey = visibleTabs.length;
|
|
40
|
+
const help = `${COLORS.dim}[Q] Quit [↑↓] Scroll [←→] Tabs [1-${maxKey}] Tabs [R] Refresh${COLORS.reset}`;
|
|
37
41
|
const fill = width - title.length - help.length - vimIndicator.length - 6;
|
|
38
42
|
return `${title}${vimIndicator}${" ".repeat(Math.max(1, fill))}${help}\n`;
|
|
39
43
|
}
|
|
@@ -49,8 +53,8 @@ export function renderSearchBar(): string {
|
|
|
49
53
|
return `\n${searchLine}${placeholder}\n`;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
export function renderTabs(activeTab: TabId): string {
|
|
53
|
-
const tabStr =
|
|
56
|
+
export function renderTabs(activeTab: TabId, visibleTabs: Tab[]): string {
|
|
57
|
+
const tabStr = visibleTabs.map((tab) => tab.id === activeTab
|
|
54
58
|
? `${COLORS.bgBlue} ${tab.key}:${tab.name} ${COLORS.reset}`
|
|
55
59
|
: `${COLORS.dim}[${tab.key}]${COLORS.reset} ${tab.name}`).join(" ");
|
|
56
60
|
return `${" ".repeat(2)}${tabStr}\n`;
|
|
@@ -507,31 +511,124 @@ export function renderClients(stats: UsageStats, width: number): string {
|
|
|
507
511
|
return output;
|
|
508
512
|
}
|
|
509
513
|
|
|
514
|
+
export function renderNpubs(stats: UsageStats, clients: ClientInfo[], width: number): string {
|
|
515
|
+
const npubStats = getNpubStats(stats.entries, clients);
|
|
516
|
+
if (npubStats.length === 0) return renderBox(["No npub data available"], width, "Npub Breakdown");
|
|
517
|
+
|
|
518
|
+
const totalCost = stats.totalSatsCost;
|
|
519
|
+
const maxCost = npubStats[0]!.satsCost;
|
|
520
|
+
const lines: string[] = [];
|
|
521
|
+
|
|
522
|
+
const col1 = 24; // Npub (truncated)
|
|
523
|
+
const col2 = 12; // Requests
|
|
524
|
+
const col3 = 24; // Cost
|
|
525
|
+
const col4 = 12; // Tokens
|
|
526
|
+
|
|
527
|
+
const hNpub = "Npub".padEnd(col1);
|
|
528
|
+
const hReqs = "Requests".padEnd(col2);
|
|
529
|
+
const hCost = "Cost".padEnd(col3);
|
|
530
|
+
const hTok = "Tokens".padEnd(col4);
|
|
531
|
+
lines.push(`${COLORS.bold}${hNpub}${hReqs}${hCost}${hTok}Avg Cost${COLORS.reset}`);
|
|
532
|
+
lines.push(COLORS.dim + "─".repeat(Math.max(0, width - 4)) + COLORS.reset);
|
|
533
|
+
|
|
534
|
+
startBarSection("npub-detail", col1);
|
|
535
|
+
for (const npub of npubStats) {
|
|
536
|
+
const pct = totalCost > 0 ? ((npub.satsCost / totalCost) * 100).toFixed(1) : "0.0";
|
|
537
|
+
const avgCostFormatted = formatCost(npub.requests > 0 ? npub.satsCost / npub.requests : 0);
|
|
538
|
+
const shortNpub = truncateNpub(npub.npub);
|
|
539
|
+
|
|
540
|
+
const dNpub = shortNpub.padEnd(col1);
|
|
541
|
+
const dReqs = formatReqs(npub.requests).padEnd(col2);
|
|
542
|
+
const dCost = `${formatCost(npub.satsCost)} sats (${pct}%)`.padEnd(col3);
|
|
543
|
+
const dTok = formatNumber(npub.totalTokens).padEnd(col4);
|
|
544
|
+
const dAvg = `${avgCostFormatted} sats/req`;
|
|
545
|
+
|
|
546
|
+
lines.push(
|
|
547
|
+
`${COLORS.magenta}${COLORS.bold}${dNpub}${COLORS.reset}` +
|
|
548
|
+
`${dReqs}` +
|
|
549
|
+
`${COLORS.green}${dCost}${COLORS.reset}` +
|
|
550
|
+
`${COLORS.dim}${dTok}${dAvg}${COLORS.reset}`
|
|
551
|
+
);
|
|
552
|
+
// Full npub on its own line for copy-ability
|
|
553
|
+
lines.push(` ${COLORS.dim}${npub.npub}${COLORS.reset}`);
|
|
554
|
+
lines.push(` ${renderBarChart("", npub.satsCost, maxCost, width - 6, COLORS.magenta, Number(pct), "npub-detail")}`);
|
|
555
|
+
lines.push("");
|
|
556
|
+
}
|
|
557
|
+
endBarSection("npub-detail");
|
|
558
|
+
|
|
559
|
+
let output = renderBox(lines, width, "Npub Breakdown");
|
|
560
|
+
|
|
561
|
+
// Top models per npub (same pattern as clients tab)
|
|
562
|
+
const npubModelMap = new Map<string, Map<string, { requests: number; satsCost: number; tokens: number }>>();
|
|
563
|
+
const clientNpubMap = new Map<string, string>();
|
|
564
|
+
for (const c of clients) {
|
|
565
|
+
if (c.ownerNpub) clientNpubMap.set(c.clientId, c.ownerNpub);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
for (const entry of stats.entries) {
|
|
569
|
+
const npub = clientNpubMap.get(entry.client || "");
|
|
570
|
+
if (!npub) continue;
|
|
571
|
+
const model = entry.modelId;
|
|
572
|
+
if (!npubModelMap.has(npub)) npubModelMap.set(npub, new Map());
|
|
573
|
+
const modelMap = npubModelMap.get(npub)!;
|
|
574
|
+
const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 };
|
|
575
|
+
modelMap.set(model, {
|
|
576
|
+
requests: existing.requests + 1,
|
|
577
|
+
satsCost: existing.satsCost + entry.satsCost,
|
|
578
|
+
tokens: existing.tokens + entry.totalTokens,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const npubModelLines: string[] = [];
|
|
583
|
+
for (const topNpub of npubStats.slice(0, 5)) {
|
|
584
|
+
const modelMap = npubModelMap.get(topNpub.npub);
|
|
585
|
+
if (!modelMap) continue;
|
|
586
|
+
const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5);
|
|
587
|
+
npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`);
|
|
588
|
+
for (const [model, data] of models) {
|
|
589
|
+
npubModelLines.push(` ${(MODEL_COLORS[model] || MODEL_COLORS.default)}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`);
|
|
590
|
+
}
|
|
591
|
+
npubModelLines.push("");
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (npubModelLines.length > 0) {
|
|
595
|
+
output += "\n" + renderBox(npubModelLines, width, "Top Models per Npub");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return output;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Truncate an npub for display: first 10 chars + … + last 6 chars. */
|
|
602
|
+
function truncateNpub(npub: string): string {
|
|
603
|
+
if (npub.length <= 24) return npub;
|
|
604
|
+
return npub.slice(0, 10) + "…" + npub.slice(-6);
|
|
605
|
+
}
|
|
606
|
+
|
|
510
607
|
export function renderRecent(stats: UsageStats, width: number): string {
|
|
511
608
|
const recentEntries = stats.entries.slice(0, 50);
|
|
512
609
|
if (recentEntries.length === 0) return renderBox(["No recent entries"], width, "Recent Requests");
|
|
513
610
|
|
|
514
|
-
const clientCol =
|
|
611
|
+
const clientCol = 14;
|
|
515
612
|
const lines: string[] = [];
|
|
516
|
-
lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"
|
|
613
|
+
lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"MODEL".padEnd(18)} ${"TOKENS".padEnd(10)} ${"COST".padEnd(12)} ${"PROVIDER".padEnd(16)} ${"CLIENT".slice(0, clientCol)}${COLORS.reset}`);
|
|
517
614
|
lines.push(COLORS.dim + "─".repeat(width - 4) + COLORS.reset);
|
|
518
615
|
|
|
519
616
|
for (const entry of recentEntries) {
|
|
520
617
|
const time = formatTime(entry.timestamp).slice(0, 8);
|
|
521
|
-
const clientName = (entry.client || "unknown").slice(0, clientCol - 1).padEnd(clientCol);
|
|
522
|
-
const clientColor = CLIENT_COLORS[entry.client || "unknown"] || CLIENT_COLORS.default || COLORS.white;
|
|
523
618
|
const model = entry.modelId.slice(0, 18).padEnd(18);
|
|
524
619
|
const tokens = `${formatNumber(entry.totalTokens).padEnd(6)} (${formatNumber(entry.promptTokens)}+${formatNumber(entry.completionTokens)})`;
|
|
525
620
|
const cost = `${formatCost(entry.satsCost).padEnd(8)} sats`;
|
|
526
|
-
const provider = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "").slice(0,
|
|
621
|
+
const provider = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "").slice(0, 16).padEnd(16);
|
|
622
|
+
const clientName = (entry.client || "unknown").slice(0, clientCol - 1);
|
|
623
|
+
const clientColor = CLIENT_COLORS[entry.client || "unknown"] || CLIENT_COLORS.default || COLORS.white;
|
|
527
624
|
const modelColor = MODEL_COLORS[entry.modelId] || MODEL_COLORS.default;
|
|
528
|
-
lines.push(`${COLORS.dim}${time}${COLORS.reset} ${
|
|
625
|
+
lines.push(`${COLORS.dim}${time}${COLORS.reset} ${modelColor}${model}${COLORS.reset} ${tokens.padEnd(10)} ${COLORS.green}${cost}${COLORS.reset} ${COLORS.dim}${provider}${COLORS.reset} ${clientColor}${clientName}${COLORS.reset}`);
|
|
529
626
|
}
|
|
530
627
|
|
|
531
628
|
return renderBox(lines, width, `Recent Requests (${stats.entries.length} shown)`);
|
|
532
629
|
}
|
|
533
630
|
|
|
534
|
-
export function renderTabContent(activeTab: TabId, stats: UsageStats, balance: BalanceInfo | null, status: StatusInfo | null, width: number): string {
|
|
631
|
+
export function renderTabContent(activeTab: TabId, stats: UsageStats, balance: BalanceInfo | null, status: StatusInfo | null, width: number, clients: ClientInfo[] = []): string {
|
|
535
632
|
switch (activeTab) {
|
|
536
633
|
case "overview": return renderOverview(stats, balance, status, width);
|
|
537
634
|
case "today": return renderToday(stats, width);
|
|
@@ -539,6 +636,7 @@ export function renderTabContent(activeTab: TabId, stats: UsageStats, balance: B
|
|
|
539
636
|
case "providers": return renderProviders(stats, width);
|
|
540
637
|
case "tokens": return renderTokens(stats, width);
|
|
541
638
|
case "clients": return renderClients(stats, width);
|
|
639
|
+
case "npubs": return renderNpubs(stats, clients, width);
|
|
542
640
|
case "recent": return renderRecent(stats, width);
|
|
543
641
|
default: return "Unknown tab";
|
|
544
642
|
}
|
package/src/tui/usage/types.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface ClientStats {
|
|
|
44
44
|
totalTokens: number;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export type TabId = "overview" | "today" | "models" | "providers" | "tokens" | "clients" | "recent";
|
|
47
|
+
export type TabId = "overview" | "today" | "models" | "providers" | "tokens" | "clients" | "npubs" | "recent";
|
|
48
48
|
|
|
49
49
|
export interface Tab {
|
|
50
50
|
id: TabId;
|
|
@@ -52,6 +52,15 @@ export interface Tab {
|
|
|
52
52
|
key: string;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export interface NpubStats {
|
|
56
|
+
npub: string;
|
|
57
|
+
requests: number;
|
|
58
|
+
satsCost: number;
|
|
59
|
+
promptTokens: number;
|
|
60
|
+
completionTokens: number;
|
|
61
|
+
totalTokens: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
export interface VimState {
|
|
56
65
|
scrollPos: number;
|
|
57
66
|
searchQuery: string;
|