routstrd 0.2.17 → 0.2.18
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/daemon/index.js +33 -6
- 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
package/dist/daemon/index.js
CHANGED
|
@@ -42670,6 +42670,18 @@ async function buildWalletDetails(deps) {
|
|
|
42670
42670
|
activeMint: deps.walletAdapter.getActiveMintUrl()
|
|
42671
42671
|
};
|
|
42672
42672
|
}
|
|
42673
|
+
function makeSdkLogger(prefix) {
|
|
42674
|
+
const tag = prefix ? `[${prefix}]` : undefined;
|
|
42675
|
+
const fmt = (...args) => tag ? [tag, ...args] : args;
|
|
42676
|
+
return {
|
|
42677
|
+
log: (...args) => logger3.log(...fmt(...args)),
|
|
42678
|
+
warn: (...args) => logger3.log(...fmt(...args)),
|
|
42679
|
+
error: (...args) => logger3.error(...fmt(...args)),
|
|
42680
|
+
debug: (...args) => logger3.debug(...fmt(...args)),
|
|
42681
|
+
child: (p) => makeSdkLogger(prefix ? `${prefix}:${p}` : p)
|
|
42682
|
+
};
|
|
42683
|
+
}
|
|
42684
|
+
var sdkLogger = makeSdkLogger();
|
|
42673
42685
|
function createDaemonRequestHandler(deps) {
|
|
42674
42686
|
return async function handler(req, res) {
|
|
42675
42687
|
const host = req.headers.host || "localhost";
|
|
@@ -43242,7 +43254,9 @@ function createDaemonRequestHandler(deps) {
|
|
|
43242
43254
|
}
|
|
43243
43255
|
try {
|
|
43244
43256
|
await deps.ensureProvidersBootstrapped();
|
|
43245
|
-
|
|
43257
|
+
const reqId = randomBytes6(4).toString("hex");
|
|
43258
|
+
const reqLogger = sdkLogger.child(`req:${reqId}`);
|
|
43259
|
+
logger3.log(`[req:${reqId}] Routing request with path: `, url2.pathname);
|
|
43246
43260
|
const response = await routeRequests({
|
|
43247
43261
|
modelId,
|
|
43248
43262
|
requestBody,
|
|
@@ -43258,7 +43272,8 @@ function createDaemonRequestHandler(deps) {
|
|
|
43258
43272
|
mode: deps.mode,
|
|
43259
43273
|
usageTrackingDriver: deps.usageTrackingDriver,
|
|
43260
43274
|
sdkStore: deps.store,
|
|
43261
|
-
providerManager: deps.providerManager
|
|
43275
|
+
providerManager: deps.providerManager,
|
|
43276
|
+
logger: reqLogger
|
|
43262
43277
|
});
|
|
43263
43278
|
res.statusCode = response.status;
|
|
43264
43279
|
response.headers.forEach((value, key) => {
|
|
@@ -43341,6 +43356,18 @@ async function refreshModelsAndIntegrations(getRoutstr21Models, config, label =
|
|
|
43341
43356
|
|
|
43342
43357
|
// src/daemon/index.ts
|
|
43343
43358
|
init_dist3();
|
|
43359
|
+
function makeSdkLogger2(prefix) {
|
|
43360
|
+
const tag = prefix ? `[${prefix}]` : undefined;
|
|
43361
|
+
const fmt = (...args) => tag ? [tag, ...args] : args;
|
|
43362
|
+
return {
|
|
43363
|
+
log: (...args) => logger3.log(...fmt(...args)),
|
|
43364
|
+
warn: (...args) => logger3.log(...fmt(...args)),
|
|
43365
|
+
error: (...args) => logger3.error(...fmt(...args)),
|
|
43366
|
+
debug: (...args) => logger3.debug(...fmt(...args)),
|
|
43367
|
+
child: (p) => makeSdkLogger2(prefix ? `${prefix}:${p}` : p)
|
|
43368
|
+
};
|
|
43369
|
+
}
|
|
43370
|
+
var daemonSdkLogger = makeSdkLogger2();
|
|
43344
43371
|
async function main() {
|
|
43345
43372
|
const args = parseArgs(process.argv);
|
|
43346
43373
|
const config = await loadDaemonConfig();
|
|
@@ -43349,7 +43376,7 @@ async function main() {
|
|
|
43349
43376
|
await ensureDirs();
|
|
43350
43377
|
const updatedConfig = { ...config, port, provider };
|
|
43351
43378
|
saveDaemonConfig(updatedConfig);
|
|
43352
|
-
const sqliteDriver = await createBunSqliteDriver2(DB_PATH);
|
|
43379
|
+
const sqliteDriver = await createBunSqliteDriver2(DB_PATH, { logger: daemonSdkLogger });
|
|
43353
43380
|
const { store, hydrate } = createSdkStore({ driver: sqliteDriver });
|
|
43354
43381
|
await hydrate;
|
|
43355
43382
|
const { Database } = await import("bun:sqlite");
|
|
@@ -43359,10 +43386,10 @@ async function main() {
|
|
|
43359
43386
|
legacyStorageDriver: sqliteDriver
|
|
43360
43387
|
});
|
|
43361
43388
|
const discoveryAdapter = createDiscoveryAdapterFromStore(store);
|
|
43362
|
-
const providerRegistry = createProviderRegistryFromStore(store);
|
|
43389
|
+
const providerRegistry = createProviderRegistryFromStore(store, daemonSdkLogger);
|
|
43363
43390
|
const storageAdapter = createStorageAdapterFromStore(store);
|
|
43364
|
-
const modelManager = new ModelManager(discoveryAdapter);
|
|
43365
|
-
const providerManager = new ProviderManager(providerRegistry, store);
|
|
43391
|
+
const modelManager = new ModelManager(discoveryAdapter, { logger: daemonSdkLogger });
|
|
43392
|
+
const providerManager = new ProviderManager(providerRegistry, store, daemonSdkLogger);
|
|
43366
43393
|
const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders } = createModelService(modelManager, store);
|
|
43367
43394
|
const walletClient = createCocodClient({ cocodPath: config.cocodPath });
|
|
43368
43395
|
const walletAdapter = await createWalletAdapter({
|
package/dist/index.js
CHANGED
|
@@ -2302,14 +2302,6 @@ var init_process_lock = () => {
|
|
|
2302
2302
|
};
|
|
2303
2303
|
|
|
2304
2304
|
// src/start-daemon.ts
|
|
2305
|
-
import { existsSync as existsSync2 } from "fs";
|
|
2306
|
-
function getTodayLogFile() {
|
|
2307
|
-
const now = new Date;
|
|
2308
|
-
const year = now.getFullYear();
|
|
2309
|
-
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
2310
|
-
const day = String(now.getDate()).padStart(2, "0");
|
|
2311
|
-
return `${LOGS_DIR2}/${year}-${month}-${day}.log`;
|
|
2312
|
-
}
|
|
2313
2305
|
async function isDaemonHealthy(port) {
|
|
2314
2306
|
const controller = new AbortController;
|
|
2315
2307
|
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
@@ -2339,12 +2331,8 @@ async function startDaemonUnlocked(options) {
|
|
|
2339
2331
|
if (options.provider) {
|
|
2340
2332
|
args.push("--provider", options.provider);
|
|
2341
2333
|
}
|
|
2342
|
-
if (!existsSync2(LOGS_DIR2)) {
|
|
2343
|
-
await Bun.$`mkdir -p ${LOGS_DIR2}`;
|
|
2344
|
-
}
|
|
2345
2334
|
const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname;
|
|
2346
|
-
const
|
|
2347
|
-
const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")} >> "${todayLogFile}" 2>&1`;
|
|
2335
|
+
const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")}`;
|
|
2348
2336
|
const proc = Bun.spawn(["sh", "-c", shellCmd], {
|
|
2349
2337
|
stdout: "inherit",
|
|
2350
2338
|
stderr: "inherit",
|
|
@@ -9380,10 +9368,10 @@ var init_nip98 = __esm(() => {
|
|
|
9380
9368
|
});
|
|
9381
9369
|
|
|
9382
9370
|
// src/utils/daemon-client.ts
|
|
9383
|
-
import { existsSync as
|
|
9371
|
+
import { existsSync as existsSync2 } from "fs";
|
|
9384
9372
|
async function loadConfig() {
|
|
9385
9373
|
try {
|
|
9386
|
-
if (
|
|
9374
|
+
if (existsSync2(CONFIG_FILE)) {
|
|
9387
9375
|
const content = await Bun.file(CONFIG_FILE).text();
|
|
9388
9376
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
9389
9377
|
}
|
|
@@ -13681,17 +13669,23 @@ var require_browser = __commonJS((exports) => {
|
|
|
13681
13669
|
});
|
|
13682
13670
|
|
|
13683
13671
|
// src/tui/usage/constants.ts
|
|
13684
|
-
|
|
13672
|
+
function getVisibleTabs(hasNpubs) {
|
|
13673
|
+
const tabs = ALL_TABS.filter((t) => t.id !== "npubs" || hasNpubs);
|
|
13674
|
+
return tabs.map((t, i2) => ({ ...t, key: String(i2 + 1) }));
|
|
13675
|
+
}
|
|
13676
|
+
var ALL_TABS, TABS, COLORS, MODEL_COLORS, CLIENT_COLORS;
|
|
13685
13677
|
var init_constants = __esm(() => {
|
|
13686
|
-
|
|
13678
|
+
ALL_TABS = [
|
|
13687
13679
|
{ id: "overview", name: "Overview", key: "1" },
|
|
13688
13680
|
{ id: "today", name: "Today", key: "2" },
|
|
13689
13681
|
{ id: "models", name: "Models", key: "3" },
|
|
13690
13682
|
{ id: "providers", name: "Providers", key: "4" },
|
|
13691
13683
|
{ id: "tokens", name: "Tokens", key: "5" },
|
|
13692
13684
|
{ id: "clients", name: "Clients", key: "6" },
|
|
13693
|
-
{ id: "
|
|
13685
|
+
{ id: "npubs", name: "Npubs", key: "7" },
|
|
13686
|
+
{ id: "recent", name: "Recent", key: "8" }
|
|
13694
13687
|
];
|
|
13688
|
+
TABS = getVisibleTabs(false);
|
|
13695
13689
|
COLORS = {
|
|
13696
13690
|
reset: "\x1B[0m",
|
|
13697
13691
|
bold: "\x1B[1m",
|
|
@@ -13915,6 +13909,58 @@ function getClientStats(entries) {
|
|
|
13915
13909
|
}
|
|
13916
13910
|
return Array.from(clients.values()).sort((a, b) => b.satsCost - a.satsCost);
|
|
13917
13911
|
}
|
|
13912
|
+
async function fetchClients() {
|
|
13913
|
+
try {
|
|
13914
|
+
const running = await isDaemonRunning();
|
|
13915
|
+
if (!running)
|
|
13916
|
+
return [];
|
|
13917
|
+
const result = await callDaemon("/clients");
|
|
13918
|
+
if (result.error)
|
|
13919
|
+
return [];
|
|
13920
|
+
const output = result.output;
|
|
13921
|
+
return (output?.clients || []).map((c) => ({
|
|
13922
|
+
clientId: c.id,
|
|
13923
|
+
name: c.name,
|
|
13924
|
+
ownerNpub: c.ownerNpub
|
|
13925
|
+
}));
|
|
13926
|
+
} catch {
|
|
13927
|
+
return [];
|
|
13928
|
+
}
|
|
13929
|
+
}
|
|
13930
|
+
function hasAnyNpubs(clients) {
|
|
13931
|
+
return clients.some((c) => !!c.ownerNpub);
|
|
13932
|
+
}
|
|
13933
|
+
function getNpubStats(entries, clients) {
|
|
13934
|
+
const clientNpubMap = new Map;
|
|
13935
|
+
for (const c of clients) {
|
|
13936
|
+
if (c.ownerNpub) {
|
|
13937
|
+
clientNpubMap.set(c.clientId, c.ownerNpub);
|
|
13938
|
+
}
|
|
13939
|
+
}
|
|
13940
|
+
const npubs = new Map;
|
|
13941
|
+
for (const entry of entries) {
|
|
13942
|
+
const npub = clientNpubMap.get(entry.client || "");
|
|
13943
|
+
if (!npub)
|
|
13944
|
+
continue;
|
|
13945
|
+
const existing = npubs.get(npub) || {
|
|
13946
|
+
npub,
|
|
13947
|
+
requests: 0,
|
|
13948
|
+
satsCost: 0,
|
|
13949
|
+
promptTokens: 0,
|
|
13950
|
+
completionTokens: 0,
|
|
13951
|
+
totalTokens: 0
|
|
13952
|
+
};
|
|
13953
|
+
npubs.set(npub, {
|
|
13954
|
+
...existing,
|
|
13955
|
+
requests: existing.requests + 1,
|
|
13956
|
+
satsCost: existing.satsCost + entry.satsCost,
|
|
13957
|
+
promptTokens: existing.promptTokens + entry.promptTokens,
|
|
13958
|
+
completionTokens: existing.completionTokens + entry.completionTokens,
|
|
13959
|
+
totalTokens: existing.totalTokens + entry.totalTokens
|
|
13960
|
+
});
|
|
13961
|
+
}
|
|
13962
|
+
return Array.from(npubs.values()).sort((a, b) => b.satsCost - a.satsCost);
|
|
13963
|
+
}
|
|
13918
13964
|
function getTotals(entries) {
|
|
13919
13965
|
if (!entries || !Array.isArray(entries)) {
|
|
13920
13966
|
return { promptTokens: 0, completionTokens: 0, totalTokens: 0, satsCost: 0 };
|
|
@@ -14061,10 +14107,11 @@ function formatReqs(value) {
|
|
|
14061
14107
|
return (value / 1000).toFixed(1) + "k";
|
|
14062
14108
|
return value.toString();
|
|
14063
14109
|
}
|
|
14064
|
-
function renderHeader(activeTab, width) {
|
|
14110
|
+
function renderHeader(activeTab, width, visibleTabs) {
|
|
14065
14111
|
const title = `${COLORS.bold}${COLORS.cyan}ROUTSTRD USAGE MONITOR${COLORS.reset}`;
|
|
14066
14112
|
const vimIndicator = `${COLORS.yellow}[vim]${COLORS.reset}`;
|
|
14067
|
-
const
|
|
14113
|
+
const maxKey = visibleTabs.length;
|
|
14114
|
+
const help = `${COLORS.dim}[Q] Quit [\u2191\u2193] Scroll [\u2190\u2192] Tabs [1-${maxKey}] Tabs [R] Refresh${COLORS.reset}`;
|
|
14068
14115
|
const fill = width - title.length - help.length - vimIndicator.length - 6;
|
|
14069
14116
|
return `${title}${vimIndicator}${" ".repeat(Math.max(1, fill))}${help}
|
|
14070
14117
|
`;
|
|
@@ -14080,8 +14127,8 @@ function renderSearchBar() {
|
|
|
14080
14127
|
${searchLine}${placeholder}
|
|
14081
14128
|
`;
|
|
14082
14129
|
}
|
|
14083
|
-
function renderTabs(activeTab) {
|
|
14084
|
-
const tabStr =
|
|
14130
|
+
function renderTabs(activeTab, visibleTabs) {
|
|
14131
|
+
const tabStr = visibleTabs.map((tab) => tab.id === activeTab ? `${COLORS.bgBlue} ${tab.key}:${tab.name} ${COLORS.reset}` : `${COLORS.dim}[${tab.key}]${COLORS.reset} ${tab.name}`).join(" ");
|
|
14085
14132
|
return `${" ".repeat(2)}${tabStr}
|
|
14086
14133
|
`;
|
|
14087
14134
|
}
|
|
@@ -14452,28 +14499,106 @@ function renderClients(stats, width) {
|
|
|
14452
14499
|
}
|
|
14453
14500
|
return output;
|
|
14454
14501
|
}
|
|
14502
|
+
function renderNpubs(stats, clients, width) {
|
|
14503
|
+
const npubStats = getNpubStats(stats.entries, clients);
|
|
14504
|
+
if (npubStats.length === 0)
|
|
14505
|
+
return renderBox(["No npub data available"], width, "Npub Breakdown");
|
|
14506
|
+
const totalCost = stats.totalSatsCost;
|
|
14507
|
+
const maxCost = npubStats[0].satsCost;
|
|
14508
|
+
const lines = [];
|
|
14509
|
+
const col1 = 24;
|
|
14510
|
+
const col2 = 12;
|
|
14511
|
+
const col3 = 24;
|
|
14512
|
+
const col4 = 12;
|
|
14513
|
+
const hNpub = "Npub".padEnd(col1);
|
|
14514
|
+
const hReqs = "Requests".padEnd(col2);
|
|
14515
|
+
const hCost = "Cost".padEnd(col3);
|
|
14516
|
+
const hTok = "Tokens".padEnd(col4);
|
|
14517
|
+
lines.push(`${COLORS.bold}${hNpub}${hReqs}${hCost}${hTok}Avg Cost${COLORS.reset}`);
|
|
14518
|
+
lines.push(COLORS.dim + "\u2500".repeat(Math.max(0, width - 4)) + COLORS.reset);
|
|
14519
|
+
startBarSection("npub-detail", col1);
|
|
14520
|
+
for (const npub of npubStats) {
|
|
14521
|
+
const pct = totalCost > 0 ? (npub.satsCost / totalCost * 100).toFixed(1) : "0.0";
|
|
14522
|
+
const avgCostFormatted = formatCost(npub.requests > 0 ? npub.satsCost / npub.requests : 0);
|
|
14523
|
+
const shortNpub = truncateNpub(npub.npub);
|
|
14524
|
+
const dNpub = shortNpub.padEnd(col1);
|
|
14525
|
+
const dReqs = formatReqs(npub.requests).padEnd(col2);
|
|
14526
|
+
const dCost = `${formatCost(npub.satsCost)} sats (${pct}%)`.padEnd(col3);
|
|
14527
|
+
const dTok = formatNumber(npub.totalTokens).padEnd(col4);
|
|
14528
|
+
const dAvg = `${avgCostFormatted} sats/req`;
|
|
14529
|
+
lines.push(`${COLORS.magenta}${COLORS.bold}${dNpub}${COLORS.reset}` + `${dReqs}` + `${COLORS.green}${dCost}${COLORS.reset}` + `${COLORS.dim}${dTok}${dAvg}${COLORS.reset}`);
|
|
14530
|
+
lines.push(` ${COLORS.dim}${npub.npub}${COLORS.reset}`);
|
|
14531
|
+
lines.push(` ${renderBarChart("", npub.satsCost, maxCost, width - 6, COLORS.magenta, Number(pct), "npub-detail")}`);
|
|
14532
|
+
lines.push("");
|
|
14533
|
+
}
|
|
14534
|
+
endBarSection("npub-detail");
|
|
14535
|
+
let output = renderBox(lines, width, "Npub Breakdown");
|
|
14536
|
+
const npubModelMap = new Map;
|
|
14537
|
+
const clientNpubMap = new Map;
|
|
14538
|
+
for (const c of clients) {
|
|
14539
|
+
if (c.ownerNpub)
|
|
14540
|
+
clientNpubMap.set(c.clientId, c.ownerNpub);
|
|
14541
|
+
}
|
|
14542
|
+
for (const entry of stats.entries) {
|
|
14543
|
+
const npub = clientNpubMap.get(entry.client || "");
|
|
14544
|
+
if (!npub)
|
|
14545
|
+
continue;
|
|
14546
|
+
const model = entry.modelId;
|
|
14547
|
+
if (!npubModelMap.has(npub))
|
|
14548
|
+
npubModelMap.set(npub, new Map);
|
|
14549
|
+
const modelMap = npubModelMap.get(npub);
|
|
14550
|
+
const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 };
|
|
14551
|
+
modelMap.set(model, {
|
|
14552
|
+
requests: existing.requests + 1,
|
|
14553
|
+
satsCost: existing.satsCost + entry.satsCost,
|
|
14554
|
+
tokens: existing.tokens + entry.totalTokens
|
|
14555
|
+
});
|
|
14556
|
+
}
|
|
14557
|
+
const npubModelLines = [];
|
|
14558
|
+
for (const topNpub of npubStats.slice(0, 5)) {
|
|
14559
|
+
const modelMap = npubModelMap.get(topNpub.npub);
|
|
14560
|
+
if (!modelMap)
|
|
14561
|
+
continue;
|
|
14562
|
+
const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5);
|
|
14563
|
+
npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`);
|
|
14564
|
+
for (const [model, data] of models) {
|
|
14565
|
+
npubModelLines.push(` ${MODEL_COLORS[model] || MODEL_COLORS.default}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`);
|
|
14566
|
+
}
|
|
14567
|
+
npubModelLines.push("");
|
|
14568
|
+
}
|
|
14569
|
+
if (npubModelLines.length > 0) {
|
|
14570
|
+
output += `
|
|
14571
|
+
` + renderBox(npubModelLines, width, "Top Models per Npub");
|
|
14572
|
+
}
|
|
14573
|
+
return output;
|
|
14574
|
+
}
|
|
14575
|
+
function truncateNpub(npub) {
|
|
14576
|
+
if (npub.length <= 24)
|
|
14577
|
+
return npub;
|
|
14578
|
+
return npub.slice(0, 10) + "\u2026" + npub.slice(-6);
|
|
14579
|
+
}
|
|
14455
14580
|
function renderRecent(stats, width) {
|
|
14456
14581
|
const recentEntries = stats.entries.slice(0, 50);
|
|
14457
14582
|
if (recentEntries.length === 0)
|
|
14458
14583
|
return renderBox(["No recent entries"], width, "Recent Requests");
|
|
14459
|
-
const clientCol =
|
|
14584
|
+
const clientCol = 14;
|
|
14460
14585
|
const lines = [];
|
|
14461
|
-
lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"
|
|
14586
|
+
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}`);
|
|
14462
14587
|
lines.push(COLORS.dim + "\u2500".repeat(width - 4) + COLORS.reset);
|
|
14463
14588
|
for (const entry of recentEntries) {
|
|
14464
14589
|
const time = formatTime(entry.timestamp).slice(0, 8);
|
|
14465
|
-
const clientName = (entry.client || "unknown").slice(0, clientCol - 1).padEnd(clientCol);
|
|
14466
|
-
const clientColor = CLIENT_COLORS[entry.client || "unknown"] || CLIENT_COLORS.default || COLORS.white;
|
|
14467
14590
|
const model = entry.modelId.slice(0, 18).padEnd(18);
|
|
14468
14591
|
const tokens = `${formatNumber(entry.totalTokens).padEnd(6)} (${formatNumber(entry.promptTokens)}+${formatNumber(entry.completionTokens)})`;
|
|
14469
14592
|
const cost = `${formatCost(entry.satsCost).padEnd(8)} sats`;
|
|
14470
|
-
const provider = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "").slice(0,
|
|
14593
|
+
const provider = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "").slice(0, 16).padEnd(16);
|
|
14594
|
+
const clientName = (entry.client || "unknown").slice(0, clientCol - 1);
|
|
14595
|
+
const clientColor = CLIENT_COLORS[entry.client || "unknown"] || CLIENT_COLORS.default || COLORS.white;
|
|
14471
14596
|
const modelColor = MODEL_COLORS[entry.modelId] || MODEL_COLORS.default;
|
|
14472
|
-
lines.push(`${COLORS.dim}${time}${COLORS.reset} ${
|
|
14597
|
+
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}`);
|
|
14473
14598
|
}
|
|
14474
14599
|
return renderBox(lines, width, `Recent Requests (${stats.entries.length} shown)`);
|
|
14475
14600
|
}
|
|
14476
|
-
function renderTabContent(activeTab, stats, balance, status, width) {
|
|
14601
|
+
function renderTabContent(activeTab, stats, balance, status, width, clients = []) {
|
|
14477
14602
|
switch (activeTab) {
|
|
14478
14603
|
case "overview":
|
|
14479
14604
|
return renderOverview(stats, balance, status, width);
|
|
@@ -14487,6 +14612,8 @@ function renderTabContent(activeTab, stats, balance, status, width) {
|
|
|
14487
14612
|
return renderTokens(stats, width);
|
|
14488
14613
|
case "clients":
|
|
14489
14614
|
return renderClients(stats, width);
|
|
14615
|
+
case "npubs":
|
|
14616
|
+
return renderNpubs(stats, clients, width);
|
|
14490
14617
|
case "recent":
|
|
14491
14618
|
return renderRecent(stats, width);
|
|
14492
14619
|
default:
|
|
@@ -14516,6 +14643,8 @@ async function runUsageTui() {
|
|
|
14516
14643
|
let stats = null;
|
|
14517
14644
|
let balance = null;
|
|
14518
14645
|
let status = null;
|
|
14646
|
+
let clients = [];
|
|
14647
|
+
let visibleTabs = getVisibleTabs(false);
|
|
14519
14648
|
let refreshInterval = null;
|
|
14520
14649
|
let shouldUpdate = true;
|
|
14521
14650
|
let autoRefresh = true;
|
|
@@ -14555,6 +14684,13 @@ async function runUsageTui() {
|
|
|
14555
14684
|
stats = await fetchUsage(1e4);
|
|
14556
14685
|
balance = await fetchBalance();
|
|
14557
14686
|
status = await fetchStatus();
|
|
14687
|
+
clients = await fetchClients();
|
|
14688
|
+
const npubsVisible = hasAnyNpubs(clients);
|
|
14689
|
+
visibleTabs = getVisibleTabs(npubsVisible);
|
|
14690
|
+
if (currentTab === "npubs" && !npubsVisible) {
|
|
14691
|
+
currentTab = "clients";
|
|
14692
|
+
vimState.scrollPos = 0;
|
|
14693
|
+
}
|
|
14558
14694
|
shouldUpdate = false;
|
|
14559
14695
|
}
|
|
14560
14696
|
if (!stats) {
|
|
@@ -14564,9 +14700,9 @@ async function runUsageTui() {
|
|
|
14564
14700
|
Press Q to quit.`);
|
|
14565
14701
|
return;
|
|
14566
14702
|
}
|
|
14567
|
-
const content = renderTabContent(currentTab, stats, balance, status, width);
|
|
14703
|
+
const content = renderTabContent(currentTab, stats, balance, status, width, clients);
|
|
14568
14704
|
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}` : ""}`;
|
|
14569
|
-
const chrome = renderHeader(currentTab, width) + renderTabs(currentTab) + renderSeparator(width) + renderSearchBar();
|
|
14705
|
+
const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar();
|
|
14570
14706
|
const chromeLines = chrome.split(`
|
|
14571
14707
|
`).length - 1;
|
|
14572
14708
|
const footerSeparator = renderSeparator(width);
|
|
@@ -14640,15 +14776,15 @@ Press Q to quit.`);
|
|
|
14640
14776
|
return;
|
|
14641
14777
|
}
|
|
14642
14778
|
if (key === "l" || key === "\x1B[C" || key === "\x1BOC") {
|
|
14643
|
-
const currentIdx =
|
|
14644
|
-
currentTab =
|
|
14779
|
+
const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
|
|
14780
|
+
currentTab = visibleTabs[(currentIdx + 1) % visibleTabs.length].id;
|
|
14645
14781
|
vimState.scrollPos = 0;
|
|
14646
14782
|
render2(false);
|
|
14647
14783
|
return;
|
|
14648
14784
|
}
|
|
14649
14785
|
if (key === "h" || key === "\x1B[D" || key === "\x1BOD") {
|
|
14650
|
-
const currentIdx =
|
|
14651
|
-
currentTab =
|
|
14786
|
+
const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
|
|
14787
|
+
currentTab = visibleTabs[(currentIdx - 1 + visibleTabs.length) % visibleTabs.length].id;
|
|
14652
14788
|
vimState.scrollPos = 0;
|
|
14653
14789
|
render2(false);
|
|
14654
14790
|
return;
|
|
@@ -14727,7 +14863,7 @@ Press Q to quit.`);
|
|
|
14727
14863
|
render2(false);
|
|
14728
14864
|
return;
|
|
14729
14865
|
}
|
|
14730
|
-
const tab =
|
|
14866
|
+
const tab = visibleTabs.find((t) => t.key === key);
|
|
14731
14867
|
if (tab) {
|
|
14732
14868
|
currentTab = tab.id;
|
|
14733
14869
|
vimState.scrollPos = 0;
|
|
@@ -14750,7 +14886,6 @@ var init_app = __esm(() => {
|
|
|
14750
14886
|
init_state();
|
|
14751
14887
|
init_constants();
|
|
14752
14888
|
init_render();
|
|
14753
|
-
init_daemon_client();
|
|
14754
14889
|
});
|
|
14755
14890
|
|
|
14756
14891
|
// src/tui/usage/index.ts
|
|
@@ -14792,7 +14927,7 @@ import { join as join3 } from "path";
|
|
|
14792
14927
|
// src/integrations/opencode.ts
|
|
14793
14928
|
init_logger();
|
|
14794
14929
|
init_daemon_client();
|
|
14795
|
-
import { existsSync as
|
|
14930
|
+
import { existsSync as existsSync3, mkdirSync } from "fs";
|
|
14796
14931
|
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
14797
14932
|
import { dirname as dirname2 } from "path";
|
|
14798
14933
|
var OPENCODE_SMALL_MODEL = "routstr/minimax-m2.5";
|
|
@@ -14804,7 +14939,7 @@ Installing routstr models in opencode.json...`);
|
|
|
14804
14939
|
const baseUrl = getDaemonBaseUrl(config);
|
|
14805
14940
|
let opencodeConfig;
|
|
14806
14941
|
try {
|
|
14807
|
-
if (
|
|
14942
|
+
if (existsSync3(configPath)) {
|
|
14808
14943
|
const content = await readFile2(configPath, "utf-8");
|
|
14809
14944
|
opencodeConfig = JSON.parse(content);
|
|
14810
14945
|
} else {
|
|
@@ -14849,7 +14984,7 @@ Installing routstr models in opencode.json...`);
|
|
|
14849
14984
|
// src/integrations/pi.ts
|
|
14850
14985
|
init_logger();
|
|
14851
14986
|
init_daemon_client();
|
|
14852
|
-
import { existsSync as
|
|
14987
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
14853
14988
|
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
14854
14989
|
import { dirname as dirname3 } from "path";
|
|
14855
14990
|
async function installPiIntegration(config, apiKey, integrationConfig) {
|
|
@@ -14860,7 +14995,7 @@ Installing routstr models in pi models.json...`);
|
|
|
14860
14995
|
const baseUrl = `${getDaemonBaseUrl(config)}/v1`;
|
|
14861
14996
|
let piConfig = {};
|
|
14862
14997
|
try {
|
|
14863
|
-
if (
|
|
14998
|
+
if (existsSync4(configPath)) {
|
|
14864
14999
|
const content = await readFile3(configPath, "utf-8");
|
|
14865
15000
|
piConfig = JSON.parse(content);
|
|
14866
15001
|
}
|
|
@@ -14897,7 +15032,7 @@ Installing routstr models in pi models.json...`);
|
|
|
14897
15032
|
// src/integrations/openclaw.ts
|
|
14898
15033
|
init_logger();
|
|
14899
15034
|
init_daemon_client();
|
|
14900
|
-
import { existsSync as
|
|
15035
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
14901
15036
|
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
14902
15037
|
import { dirname as dirname4 } from "path";
|
|
14903
15038
|
var OPENCLAW_PROVIDER_ID = "routstr";
|
|
@@ -14911,7 +15046,7 @@ Installing routstr models in openclaw.json...`);
|
|
|
14911
15046
|
const baseUrl = getDaemonBaseUrl(config);
|
|
14912
15047
|
let openclawConfig = {};
|
|
14913
15048
|
try {
|
|
14914
|
-
if (
|
|
15049
|
+
if (existsSync5(configPath)) {
|
|
14915
15050
|
const content = await readFile4(configPath, "utf-8");
|
|
14916
15051
|
openclawConfig = JSON.parse(content);
|
|
14917
15052
|
}
|
|
@@ -14973,7 +15108,7 @@ Installing routstr models in openclaw.json...`);
|
|
|
14973
15108
|
// src/integrations/claudecode.ts
|
|
14974
15109
|
init_logger();
|
|
14975
15110
|
init_daemon_client();
|
|
14976
|
-
import { existsSync as
|
|
15111
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
14977
15112
|
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
14978
15113
|
import { dirname as dirname5 } from "path";
|
|
14979
15114
|
async function installClaudeCodeIntegration(config, apiKey, integrationConfig) {
|
|
@@ -14984,7 +15119,7 @@ Installing routstr configuration in ${configPath}...`);
|
|
|
14984
15119
|
const baseUrl = getDaemonBaseUrl(config);
|
|
14985
15120
|
let settings = {};
|
|
14986
15121
|
try {
|
|
14987
|
-
if (
|
|
15122
|
+
if (existsSync6(configPath)) {
|
|
14988
15123
|
const content = await readFile5(configPath, "utf-8");
|
|
14989
15124
|
settings = JSON.parse(content);
|
|
14990
15125
|
}
|
|
@@ -15031,7 +15166,7 @@ Installing routstr configuration in ${configPath}...`);
|
|
|
15031
15166
|
// src/integrations/hermes.ts
|
|
15032
15167
|
init_logger();
|
|
15033
15168
|
init_daemon_client();
|
|
15034
|
-
import { existsSync as
|
|
15169
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
15035
15170
|
import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
15036
15171
|
import { dirname as dirname6 } from "path";
|
|
15037
15172
|
async function installHermesIntegration(config, apiKey, integrationConfig) {
|
|
@@ -15060,7 +15195,7 @@ Installing routstr configuration in ${configPath}...`);
|
|
|
15060
15195
|
}
|
|
15061
15196
|
let content = "";
|
|
15062
15197
|
try {
|
|
15063
|
-
if (
|
|
15198
|
+
if (existsSync7(configPath)) {
|
|
15064
15199
|
content = await readFile6(configPath, "utf-8");
|
|
15065
15200
|
}
|
|
15066
15201
|
} catch (error) {
|
|
@@ -15312,7 +15447,7 @@ async function addClientAction(options) {
|
|
|
15312
15447
|
// src/cli.ts
|
|
15313
15448
|
init_config();
|
|
15314
15449
|
init_logger();
|
|
15315
|
-
import { existsSync as
|
|
15450
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
|
|
15316
15451
|
import { execSync } from "child_process";
|
|
15317
15452
|
|
|
15318
15453
|
// src/integrations/index.ts
|
|
@@ -15466,7 +15601,7 @@ init_nip98();
|
|
|
15466
15601
|
init_esm();
|
|
15467
15602
|
|
|
15468
15603
|
// src/daemon/wallet/cocod-client.ts
|
|
15469
|
-
import { existsSync as
|
|
15604
|
+
import { existsSync as existsSync8 } from "fs";
|
|
15470
15605
|
init_logger();
|
|
15471
15606
|
init_process_lock();
|
|
15472
15607
|
var DEFAULT_CONFIG_DIR = process.env.COCOD_DIR || `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`;
|
|
@@ -15478,7 +15613,7 @@ function resolveCocodExecutable(cocodPath) {
|
|
|
15478
15613
|
async function isCocodInstalled(cocodPath) {
|
|
15479
15614
|
const executable = resolveCocodExecutable(cocodPath);
|
|
15480
15615
|
if (executable.includes("/")) {
|
|
15481
|
-
return
|
|
15616
|
+
return existsSync8(executable);
|
|
15482
15617
|
}
|
|
15483
15618
|
try {
|
|
15484
15619
|
const proc = Bun.spawn({
|
|
@@ -15494,7 +15629,7 @@ async function isCocodInstalled(cocodPath) {
|
|
|
15494
15629
|
// package.json
|
|
15495
15630
|
var package_default = {
|
|
15496
15631
|
name: "routstrd",
|
|
15497
|
-
version: "0.2.
|
|
15632
|
+
version: "0.2.18",
|
|
15498
15633
|
module: "src/index.ts",
|
|
15499
15634
|
type: "module",
|
|
15500
15635
|
private: false,
|
|
@@ -15518,7 +15653,7 @@ var package_default = {
|
|
|
15518
15653
|
},
|
|
15519
15654
|
dependencies: {
|
|
15520
15655
|
"@cashu/cashu-ts": "^3.1.1",
|
|
15521
|
-
"@routstr/sdk": "^0.3.
|
|
15656
|
+
"@routstr/sdk": "^0.3.4",
|
|
15522
15657
|
"applesauce-core": "^5.1.0",
|
|
15523
15658
|
"applesauce-relay": "^5.1.0",
|
|
15524
15659
|
commander: "^14.0.2",
|
|
@@ -15573,11 +15708,11 @@ async function requireLocalDaemon() {
|
|
|
15573
15708
|
}
|
|
15574
15709
|
async function initDaemon() {
|
|
15575
15710
|
logger.log("Initializing routstrd...");
|
|
15576
|
-
if (!
|
|
15711
|
+
if (!existsSync9(CONFIG_DIR)) {
|
|
15577
15712
|
mkdirSync6(CONFIG_DIR, { recursive: true });
|
|
15578
15713
|
logger.log(`Created config directory: ${CONFIG_DIR}`);
|
|
15579
15714
|
}
|
|
15580
|
-
if (!
|
|
15715
|
+
if (!existsSync9(CONFIG_FILE)) {
|
|
15581
15716
|
const config2 = {
|
|
15582
15717
|
...DEFAULT_CONFIG,
|
|
15583
15718
|
cocodPath: null
|
|
@@ -15701,7 +15836,7 @@ program.command("remote <url>").description("Configure a remote daemon URL").act
|
|
|
15701
15836
|
console.error(`Invalid URL: ${url}`);
|
|
15702
15837
|
process.exit(1);
|
|
15703
15838
|
}
|
|
15704
|
-
if (!
|
|
15839
|
+
if (!existsSync9(CONFIG_DIR)) {
|
|
15705
15840
|
mkdirSync6(CONFIG_DIR, { recursive: true });
|
|
15706
15841
|
}
|
|
15707
15842
|
const config = await loadConfig();
|
|
@@ -16296,7 +16431,7 @@ serviceCmd.command("install").description("Install and start routstrd using PM2
|
|
|
16296
16431
|
const path = import.meta.require("path");
|
|
16297
16432
|
daemonPath = path.join(path.dirname(import.meta.url).replace("file://", ""), "daemon", "index.js");
|
|
16298
16433
|
}
|
|
16299
|
-
if (!
|
|
16434
|
+
if (!existsSync9(daemonPath)) {
|
|
16300
16435
|
console.error(`Could not find daemon at ${daemonPath}. Did you run 'bun run build'?`);
|
|
16301
16436
|
process.exit(1);
|
|
16302
16437
|
}
|
|
@@ -16434,14 +16569,14 @@ program.command("logs").description("View daemon logs").option("-f, --follow", "
|
|
|
16434
16569
|
const yesterday = new Date;
|
|
16435
16570
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
16436
16571
|
const yesterdayFile = getLogFileForDate2(yesterday);
|
|
16437
|
-
if (!
|
|
16572
|
+
if (!existsSync9(todayFile) && !existsSync9(yesterdayFile)) {
|
|
16438
16573
|
console.log("No log files found. Daemon may not have started yet.");
|
|
16439
16574
|
console.log(`Logs directory: ${LOGS_DIR2}`);
|
|
16440
16575
|
process.exit(1);
|
|
16441
16576
|
}
|
|
16442
16577
|
const lines = parseInt(options.lines, 10);
|
|
16443
16578
|
const logFiles = [yesterdayFile, todayFile].filter((file, index, files) => {
|
|
16444
|
-
return
|
|
16579
|
+
return existsSync9(file) && files.indexOf(file) === index;
|
|
16445
16580
|
});
|
|
16446
16581
|
if (options.follow) {
|
|
16447
16582
|
const proc2 = Bun.spawn(["tail", "-n", String(lines), "-f", todayFile], {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routstrd",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
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": "^3.1.1",
|
|
27
|
-
"@routstr/sdk": "^0.3.
|
|
27
|
+
"@routstr/sdk": "^0.3.4",
|
|
28
28
|
"applesauce-core": "^5.1.0",
|
|
29
29
|
"applesauce-relay": "^5.1.0",
|
|
30
30
|
"commander": "^14.0.2",
|
package/src/start-daemon.ts
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import { logger } from "./utils/logger";
|
|
2
|
-
import { existsSync } from "fs";
|
|
3
2
|
import { CONFIG_DIR, LOGS_DIR } from "./utils/config";
|
|
4
3
|
import { withCrossProcessLock } from "./utils/process-lock";
|
|
5
4
|
|
|
6
5
|
const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`;
|
|
7
6
|
|
|
8
|
-
function getTodayLogFile(): string {
|
|
9
|
-
const now = new Date();
|
|
10
|
-
const year = now.getFullYear();
|
|
11
|
-
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
12
|
-
const day = String(now.getDate()).padStart(2, "0");
|
|
13
|
-
return `${LOGS_DIR}/${year}-${month}-${day}.log`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
7
|
async function isDaemonHealthy(port: string): Promise<boolean> {
|
|
17
8
|
const controller = new AbortController();
|
|
18
9
|
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
@@ -48,14 +39,8 @@ async function startDaemonUnlocked(
|
|
|
48
39
|
args.push("--provider", options.provider);
|
|
49
40
|
}
|
|
50
41
|
|
|
51
|
-
// Ensure logs directory exists (logger handles date-based files)
|
|
52
|
-
if (!existsSync(LOGS_DIR)) {
|
|
53
|
-
await Bun.$`mkdir -p ${LOGS_DIR}`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
42
|
const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname;
|
|
57
|
-
const
|
|
58
|
-
const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")} >> "${todayLogFile}" 2>&1`;
|
|
43
|
+
const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")}`;
|
|
59
44
|
|
|
60
45
|
const proc = Bun.spawn(["sh", "-c", shellCmd], {
|
|
61
46
|
stdout: "inherit",
|
package/src/tui/usage/app.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { getVisibleTabs } from "./constants.ts";
|
|
2
|
+
import type { Tab } from "./types.ts";
|
|
3
|
+
import { fetchBalance, fetchClients, fetchStatus, fetchUsage, hasAnyNpubs, isDaemonRunning, type BalanceInfo, type ClientInfo, type StatusInfo } from "./data.ts";
|
|
3
4
|
import {
|
|
4
5
|
applyScrollToContent,
|
|
5
6
|
exitSearchMode,
|
|
@@ -28,7 +29,6 @@ import {
|
|
|
28
29
|
import { COLORS } from "./constants.ts";
|
|
29
30
|
import { renderHeader, renderSearchBar, renderSeparator, renderTabContent, renderTabs } from "./render.ts";
|
|
30
31
|
import type { TabId, UsageStats } from "./types.ts";
|
|
31
|
-
import { isDaemonRunning } from "../../utils/daemon-client.ts";
|
|
32
32
|
|
|
33
33
|
export async function runUsageTui(): Promise<void> {
|
|
34
34
|
const running = await isDaemonRunning();
|
|
@@ -46,6 +46,8 @@ export async function runUsageTui(): Promise<void> {
|
|
|
46
46
|
let stats: UsageStats | null = null;
|
|
47
47
|
let balance: BalanceInfo | null = null;
|
|
48
48
|
let status: StatusInfo | null = null;
|
|
49
|
+
let clients: ClientInfo[] = [];
|
|
50
|
+
let visibleTabs: Tab[] = getVisibleTabs(false);
|
|
49
51
|
let refreshInterval: ReturnType<typeof setInterval> | null = null;
|
|
50
52
|
let shouldUpdate = true;
|
|
51
53
|
let autoRefresh = true;
|
|
@@ -90,6 +92,14 @@ export async function runUsageTui(): Promise<void> {
|
|
|
90
92
|
stats = await fetchUsage(10000);
|
|
91
93
|
balance = await fetchBalance();
|
|
92
94
|
status = await fetchStatus();
|
|
95
|
+
clients = await fetchClients();
|
|
96
|
+
const npubsVisible = hasAnyNpubs(clients);
|
|
97
|
+
visibleTabs = getVisibleTabs(npubsVisible);
|
|
98
|
+
// If current tab is npubs but it's no longer visible, fall back to clients
|
|
99
|
+
if (currentTab === "npubs" && !npubsVisible) {
|
|
100
|
+
currentTab = "clients";
|
|
101
|
+
vimState.scrollPos = 0;
|
|
102
|
+
}
|
|
93
103
|
shouldUpdate = false;
|
|
94
104
|
}
|
|
95
105
|
|
|
@@ -104,9 +114,9 @@ export async function runUsageTui(): Promise<void> {
|
|
|
104
114
|
return;
|
|
105
115
|
}
|
|
106
116
|
|
|
107
|
-
const content = renderTabContent(currentTab, stats, balance, status, width);
|
|
117
|
+
const content = renderTabContent(currentTab, stats, balance, status, width, clients);
|
|
108
118
|
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}` : ""}`;
|
|
109
|
-
const chrome = renderHeader(currentTab, width) + renderTabs(currentTab) + renderSeparator(width) + renderSearchBar();
|
|
119
|
+
const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar();
|
|
110
120
|
const chromeLines = chrome.split("\n").length - 1;
|
|
111
121
|
const footerSeparator = renderSeparator(width);
|
|
112
122
|
const footerLines = footerSeparator.split("\n").length - 1;
|
|
@@ -176,15 +186,15 @@ export async function runUsageTui(): Promise<void> {
|
|
|
176
186
|
return;
|
|
177
187
|
}
|
|
178
188
|
if (key === "l" || key === "\x1b[C" || key === "\x1bOC") {
|
|
179
|
-
const currentIdx =
|
|
180
|
-
currentTab =
|
|
189
|
+
const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
|
|
190
|
+
currentTab = visibleTabs[(currentIdx + 1) % visibleTabs.length]!.id;
|
|
181
191
|
vimState.scrollPos = 0;
|
|
182
192
|
void render(false);
|
|
183
193
|
return;
|
|
184
194
|
}
|
|
185
195
|
if (key === "h" || key === "\x1b[D" || key === "\x1bOD") {
|
|
186
|
-
const currentIdx =
|
|
187
|
-
currentTab =
|
|
196
|
+
const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
|
|
197
|
+
currentTab = visibleTabs[(currentIdx - 1 + visibleTabs.length) % visibleTabs.length]!.id;
|
|
188
198
|
vimState.scrollPos = 0;
|
|
189
199
|
void render(false);
|
|
190
200
|
return;
|
|
@@ -226,7 +236,7 @@ export async function runUsageTui(): Promise<void> {
|
|
|
226
236
|
}
|
|
227
237
|
if (key === "\x1b") { scrollToTop(); void render(false); return; }
|
|
228
238
|
|
|
229
|
-
const tab =
|
|
239
|
+
const tab = visibleTabs.find((t) => t.key === key);
|
|
230
240
|
if (tab) {
|
|
231
241
|
currentTab = tab.id;
|
|
232
242
|
vimState.scrollPos = 0;
|
|
@@ -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;
|