From 0c382209af7f87eb1e7d3303b18a2906b9820ae7 Mon Sep 17 00:00:00 2001 From: Bilthon Date: Tue, 2 Jun 2026 14:43:35 -0500 Subject: [PATCH 01/42] refactor: import bun sqlite drivers from @routstr/sdk/storage/bun --- src/daemon/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 8cf95fd..0c1b16d 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -28,6 +28,8 @@ import { ensureDirs, loadDaemonConfig, loadDaemonConfigSync, saveDaemonConfig } import { createBunSqliteDriver, createBunSqliteUsageTrackingDriver, +} from "@routstr/sdk/storage/bun"; +import { createShardedDiscoveryAdapter, createProviderRegistryFromDiscoveryAdapter, } from "@routstr/sdk/storage"; @@ -54,10 +56,8 @@ async function main(): Promise { const sqliteDriver = await createBunSqliteDriver(DB_PATH, { logger: daemonSdkLogger }); const { store, hydrate } = createSdkStore({ driver: sqliteDriver }); await hydrate; - const { Database } = await import("bun:sqlite"); - const usageTrackingDriver = createBunSqliteUsageTrackingDriver({ + const usageTrackingDriver = await createBunSqliteUsageTrackingDriver({ dbPath: DB_PATH, - sqlite: { Database }, legacyStorageDriver: sqliteDriver, }); From 1b99aa166017a60f4ac5a68b22c7677821f156aa Mon Sep 17 00:00:00 2001 From: Bilthon Date: Tue, 2 Jun 2026 14:43:42 -0500 Subject: [PATCH 02/42] chore(deps): require @routstr/sdk ^0.4.0 for usage aggregate --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5423e26..eddba2d 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@cashu/cashu-ts": "^4.3.0", - "@routstr/sdk": "^0.3.9", + "@routstr/sdk": "^0.4.0", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", From 8f77da3911857060a591b0ac640b8836aabd47ba Mon Sep 17 00:00:00 2001 From: Bilthon Date: Tue, 2 Jun 2026 14:45:06 -0500 Subject: [PATCH 03/42] feat(daemon): add /usage/summary aggregated endpoint --- src/daemon/http/index.ts | 13 ++ src/daemon/http/usage-summary.ts | 302 +++++++++++++++++++++++++++++++ 2 files changed, 315 insertions(+) create mode 100644 src/daemon/http/usage-summary.ts diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 1ed8a3e..417e0ee 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -16,6 +16,7 @@ import { } from "../wallet/cocod-client"; import { decodeCashuTokenAmount } from "../wallet"; import { getClientsFromStore } from "../../utils/clients"; +import { getUsageSummary } from "./usage-summary"; type ClientMode = "xcashu" | "lazyrefund" | "apikeys"; @@ -1123,6 +1124,18 @@ export function createDaemonRequestHandler(deps: { return; } + if (req.method === "GET" && url.pathname === "/usage/summary") { + try { + const tz = Number.parseInt(url.searchParams.get("tz") || "0", 10) || 0; + const clients = getClientsFromStore(deps.store); + const summary = await getUsageSummary(deps.usageTrackingDriver, clients, tz); + sendJson(res, 200, { output: summary }); + } catch (error) { + sendJson(res, 500, { error: toErrorMessage(error) }); + } + return; + } + if (req.method === "GET" && url.pathname === "/usagePi") { try { const timestamp = (url.searchParams.get("timestamp") || "").trim(); diff --git a/src/daemon/http/usage-summary.ts b/src/daemon/http/usage-summary.ts new file mode 100644 index 0000000..ccd3023 --- /dev/null +++ b/src/daemon/http/usage-summary.ts @@ -0,0 +1,302 @@ +import type { + UsageAggregateRow, + UsageTrackingDriver, + UsageTrackingEntry, +} from "@routstr/sdk/storage"; +import type { ClientEntry } from "../../utils/clients"; + +// ─── Public shape ──────────────────────────────────────────────────────────── + +export interface StatRow { + requests: number; + promptTokens: number; + completionTokens: number; + totalTokens: number; + cost: number; + satsCost: number; +} + +export interface ModelSummary extends StatRow { + modelId: string; +} + +export interface ProviderSummary extends StatRow { + baseUrl: string; +} + +export interface TopModel { + modelId: string; + requests: number; + satsCost: number; + totalTokens: number; +} + +export interface ClientSummary extends StatRow { + client: string; + topModels: TopModel[]; +} + +export interface NpubSummary extends StatRow { + npub: string; + topModels: TopModel[]; +} + +export interface DaySummary extends StatRow { + date: string; // "YYYY-MM-DD" +} + +export interface HourSummary extends StatRow { + hour: number; // 0..23 +} + +export interface SizeBucket { + count: number; + cost: number; // summed satsCost +} + +export interface UsageSummary { + generatedAt: number; + totals: StatRow; + models: ModelSummary[]; + providers: ProviderSummary[]; + clients: ClientSummary[]; + npubs: NpubSummary[]; + days: DaySummary[]; + hoursToday: HourSummary[]; + sizeBuckets: { + tiny: SizeBucket; + small: SizeBucket; + medium: SizeBucket; + large: SizeBucket; + huge: SizeBucket; + }; + recent: UsageTrackingEntry[]; +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function rowToStat(r: UsageAggregateRow): StatRow { + return { + requests: r.requests, + promptTokens: r.promptTokens, + completionTokens: r.completionTokens, + totalTokens: r.totalTokens, + cost: r.cost, + satsCost: r.satsCost, + }; +} + +function rowToTopModel(r: UsageAggregateRow): TopModel { + return { + modelId: r.group ?? "unknown", + requests: r.requests, + satsCost: r.satsCost, + totalTokens: r.totalTokens, + }; +} + +function emptyBucket(): SizeBucket { + return { count: 0, cost: 0 }; +} + +function bucketFromRow(r: UsageAggregateRow | undefined): SizeBucket { + if (!r) return emptyBucket(); + return { count: r.requests, cost: r.satsCost }; +} + +/** Returns the UTC ms for the start of the local day containing `now`. */ +function startOfLocalDayUtc(now: number, tzOffsetMinutes: number): number { + return ( + Math.floor((now - tzOffsetMinutes * 60000) / 86400000) * 86400000 + + tzOffsetMinutes * 60000 + ); +} + +// ─── Module-level memo cache ───────────────────────────────────────────────── + +interface CacheEntry { + key: string; + summary: UsageSummary; +} + +let _cache: CacheEntry | null = null; +const CACHE_TTL_MS = 60_000; + +// ─── Main builder ───────────────────────────────────────────────────────────── + +export async function getUsageSummary( + driver: UsageTrackingDriver, + clients: ClientEntry[], + tzOffsetMinutes: number, +): Promise { + // Cheap cache key: total row count + clients length + tz + const count = await driver.count(); + const cacheKey = `${count}:${clients.length}:${tzOffsetMinutes}`; + const now = Date.now(); + + if ( + _cache !== null && + _cache.key === cacheKey && + now - _cache.summary.generatedAt <= CACHE_TTL_MS + ) { + return _cache.summary; + } + + // ── Totals ───────────────────────────────────────────────────────────────── + const [totalsRow] = await driver.aggregate({}); + const totals: StatRow = totalsRow ? rowToStat(totalsRow) : { + requests: 0, promptTokens: 0, completionTokens: 0, + totalTokens: 0, cost: 0, satsCost: 0, + }; + + // ── Models ───────────────────────────────────────────────────────────────── + const modelRows = await driver.aggregate({ groupBy: "modelId" }); + const models: ModelSummary[] = modelRows.map((r) => ({ + modelId: r.group ?? "unknown", + ...rowToStat(r), + })); + + // ── Providers ────────────────────────────────────────────────────────────── + const providerRows = await driver.aggregate({ groupBy: "baseUrl" }); + const providers: ProviderSummary[] = providerRows.map((r) => ({ + baseUrl: r.group ?? "unknown", + ...rowToStat(r), + })); + + // ── Clients ──────────────────────────────────────────────────────────────── + const clientRows = await driver.aggregate({ groupBy: "client" }); + const clientSummaries: ClientSummary[] = clientRows.map((r) => ({ + client: r.group ?? "unknown", + ...rowToStat(r), + topModels: [], + })); + + // Fill topModels for the top 3 non-null client rows + const topClientRows = clientRows + .filter((r) => r.group !== null) + .slice(0, 3); + for (let i = 0; i < topClientRows.length; i++) { + const clientId = topClientRows[i]!.group!; + const topModelRows = await driver.aggregate({ + groupBy: "modelId", + client: clientId, + }); + // Find matching ClientSummary and set topModels + const summary = clientSummaries.find((c) => c.client === clientId); + if (summary) { + summary.topModels = topModelRows.slice(0, 5).map(rowToTopModel); + } + } + + // ── Npubs ────────────────────────────────────────────────────────────────── + // Build clientId → ownerNpub lookup (only clients with ownerNpub) + const clientToNpub = new Map(); + for (const c of clients) { + if (c.ownerNpub) { + clientToNpub.set(c.clientId, c.ownerNpub); + } + } + + // Fold client rows into per-npub sums + const npubStats = new Map(); + const npubClientIds = new Map(); + for (const r of clientRows) { + if (r.group === null) continue; + const npub = clientToNpub.get(r.group); + if (!npub) continue; + const existing = npubStats.get(npub); + if (existing) { + existing.requests += r.requests; + existing.promptTokens += r.promptTokens; + existing.completionTokens += r.completionTokens; + existing.totalTokens += r.totalTokens; + existing.cost += r.cost; + existing.satsCost += r.satsCost; + } else { + npubStats.set(npub, { ...rowToStat(r) }); + } + const ids = npubClientIds.get(npub) ?? []; + ids.push(r.group); + npubClientIds.set(npub, ids); + } + + // Sort npubs desc by satsCost + const sortedNpubs = [...npubStats.entries()].sort( + (a, b) => b[1].satsCost - a[1].satsCost, + ); + + const npubs: NpubSummary[] = sortedNpubs.map(([npub, stat]) => ({ + npub, + ...stat, + topModels: [], + })); + + // Fill topModels for top 5 npubs + for (let i = 0; i < Math.min(5, npubs.length); i++) { + const npubSummary = npubs[i]!; + const ids = npubClientIds.get(npubSummary.npub) ?? []; + if (ids.length > 0) { + const topModelRows = await driver.aggregate({ + groupBy: "modelId", + clients: ids, + }); + npubSummary.topModels = topModelRows.slice(0, 5).map(rowToTopModel); + } + } + + // ── Days (last 30, most-recent-first) ───────────────────────────────────── + const dayRows = await driver.aggregate({ + groupBy: "day", + tzOffsetMinutes, + after: now - 30 * 86400000, + }); + const days: DaySummary[] = dayRows + .map((r) => ({ date: r.group!, ...rowToStat(r) })) + .reverse(); // aggregate returns ascending; we want most-recent-first + + // ── Hours today ──────────────────────────────────────────────────────────── + const todayStartUtc = startOfLocalDayUtc(now, tzOffsetMinutes); + const hourRows = await driver.aggregate({ + groupBy: "hour", + tzOffsetMinutes, + after: todayStartUtc - 1, + }); + const hoursToday: HourSummary[] = hourRows.map((r) => ({ + hour: Number(r.group), + ...rowToStat(r), + })); + + // ── Size buckets ─────────────────────────────────────────────────────────── + const [tinyRow] = await driver.aggregate({ minTotalTokens: 0, maxTotalTokens: 1000 }); + const [smallRow] = await driver.aggregate({ minTotalTokens: 1000, maxTotalTokens: 10000 }); + const [mediumRow] = await driver.aggregate({ minTotalTokens: 10000, maxTotalTokens: 50000 }); + const [largeRow] = await driver.aggregate({ minTotalTokens: 50000, maxTotalTokens: 100000 }); + const [hugeRow] = await driver.aggregate({ minTotalTokens: 100000 }); + + const sizeBuckets = { + tiny: bucketFromRow(tinyRow), + small: bucketFromRow(smallRow), + medium: bucketFromRow(mediumRow), + large: bucketFromRow(largeRow), + huge: bucketFromRow(hugeRow), + }; + + // ── Recent entries ───────────────────────────────────────────────────────── + const recent = await driver.list({ limit: 50 }); + + const summary: UsageSummary = { + generatedAt: now, + totals, + models, + providers, + clients: clientSummaries, + npubs, + days, + hoursToday, + sizeBuckets, + recent, + }; + + _cache = { key: cacheKey, summary }; + return summary; +} From 21eb88734638228ba54b851967e31092ea2601f3 Mon Sep 17 00:00:00 2001 From: Bilthon Date: Tue, 2 Jun 2026 14:51:02 -0500 Subject: [PATCH 04/42] feat(monitor): consume /usage/summary with legacy fallback --- src/tui/usage/app.ts | 4 +- src/tui/usage/data.ts | 27 ++++- src/tui/usage/render.ts | 228 ++++++++++++++++++++++++---------------- src/tui/usage/types.ts | 5 + 4 files changed, 169 insertions(+), 95 deletions(-) diff --git a/src/tui/usage/app.ts b/src/tui/usage/app.ts index e2d5f63..99faa77 100644 --- a/src/tui/usage/app.ts +++ b/src/tui/usage/app.ts @@ -1,6 +1,6 @@ import { getVisibleTabs } from "./constants.ts"; import type { Tab } from "./types.ts"; -import { fetchBalance, fetchClients, fetchStatus, fetchUsage, hasAnyNpubs, isDaemonRunning, type BalanceInfo, type ClientInfo, type StatusInfo } from "./data.ts"; +import { fetchBalance, fetchClients, fetchStatus, fetchUsage, fetchUsageSummary, hasAnyNpubs, isDaemonRunning, type BalanceInfo, type ClientInfo, type StatusInfo } from "./data.ts"; import { applyScrollToContent, exitSearchMode, @@ -89,7 +89,7 @@ export async function runUsageTui(): Promise { const height = getHeight(); if (forceFetch || shouldUpdate) { - stats = await fetchUsage(10000); + stats = await fetchUsageSummary() ?? await fetchUsage(10000); balance = await fetchBalance(); status = await fetchStatus(); clients = await fetchClients(); diff --git a/src/tui/usage/data.ts b/src/tui/usage/data.ts index 5d342f2..53f3d32 100644 --- a/src/tui/usage/data.ts +++ b/src/tui/usage/data.ts @@ -1,6 +1,6 @@ import type { UsageTrackingEntry } from "../../daemon/types.ts"; import { callDaemon, isDaemonRunning } from "../../utils/daemon-client.ts"; -import type { ClientStats, DayStats, ModelStats, NpubStats, ProviderStats, UsageStats } from "./types.ts"; +import type { ClientStats, DayStats, ModelStats, NpubStats, ProviderStats, UsageStats, UsageSummary } from "./types.ts"; export { isDaemonRunning }; @@ -112,6 +112,31 @@ export async function fetchUsage(limit = 10000): Promise { } } +export async function fetchUsageSummary(): Promise { + try { + const running = await isDaemonRunning(); + if (!running) return null; + + const tz = new Date().getTimezoneOffset(); + const result = await callDaemon(`/usage/summary?tz=${tz}`); + if (result.error) return null; + + const summary = result.output as UsageSummary | undefined; + if (!summary || typeof summary.totals !== "object") return null; + + return { + entries: summary.recent, + totalEntries: summary.totals.requests, + totalSatsCost: summary.totals.satsCost, + recentSatsCost: summary.totals.satsCost, + limit: 50, + summary, + }; + } catch { + return null; + } +} + export function getTodayStart(): number { const now = new Date(); return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); diff --git a/src/tui/usage/render.ts b/src/tui/usage/render.ts index e22cfad..0a84823 100644 --- a/src/tui/usage/render.ts +++ b/src/tui/usage/render.ts @@ -240,7 +240,7 @@ export function renderOverview(stats: UsageStats, balance: BalanceInfo | null, s output = renderBox(statusLines, width, "System Status") + "\n" + output; } - const modelStats = getModelStats(stats.entries); + const modelStats = stats.summary?.models ?? getModelStats(stats.entries); if (modelStats.length > 0) { const maxCost = modelStats[0]!.satsCost; const totalCost = Math.max(totalVisibleCost, 1); @@ -259,7 +259,7 @@ export function renderOverview(stats: UsageStats, balance: BalanceInfo | null, s output += "\n" + renderBox(modelLines, width, "Top Models by Cost"); } - const clientStats = getClientStats(stats.entries); + const clientStats = stats.summary?.clients ?? getClientStats(stats.entries); if (clientStats.length > 0) { const maxCost = clientStats[0]!.satsCost; const totalCost = Math.max(totalVisibleCost, 1); @@ -282,19 +282,40 @@ export function renderOverview(stats: UsageStats, balance: BalanceInfo | null, s } export function renderToday(stats: UsageStats, width: number): string { - const hourly = getHourlyToday(stats.entries); - const todayStart = getTodayStart(); const currentHour = new Date().getHours(); - const todayStats = { date: formatDate(Date.now()), requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 }; + const todayDateStr = formatDate(Date.now()); - for (const entry of stats.entries) { - if (entry.timestamp >= todayStart) { - todayStats.requests++; - todayStats.satsCost += entry.satsCost; - todayStats.promptTokens += entry.promptTokens; - todayStats.completionTokens += entry.completionTokens; - todayStats.totalTokens += entry.totalTokens; + let todayStats: { date: string; requests: number; satsCost: number; promptTokens: number; completionTokens: number; totalTokens: number }; + let recentDays: Array<{ date: string; requests: number; satsCost: number; totalTokens: number; promptTokens: number; completionTokens: number }>; + let hourlyMap: Map; + + if (stats.summary) { + // Use server-aggregated data + const { days, hoursToday } = stats.summary; + // days[0] is most-recent-first; check if it's today + const todayDayStat = days[0]?.date === todayDateStr ? days[0] : undefined; + 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 }; + recentDays = days.slice(1, 7); + hourlyMap = new Map(hoursToday.map((h) => [h.hour, { requests: h.requests, satsCost: h.satsCost }])); + } else { + // Fallback: compute from raw entries + const todayStart = getTodayStart(); + todayStats = { date: todayDateStr, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 }; + for (const entry of stats.entries) { + if (entry.timestamp >= todayStart) { + todayStats.requests++; + todayStats.satsCost += entry.satsCost; + todayStats.promptTokens += entry.promptTokens; + todayStats.completionTokens += entry.completionTokens; + todayStats.totalTokens += entry.totalTokens; + } } + recentDays = Array.from(getDayStats(stats.entries).values()).slice(1, 7); + hourlyMap = new Map( + Array.from(getHourlyToday(stats.entries).entries()).map(([h, s]) => [h, { requests: s.requests, satsCost: s.satsCost }]) + ); } const summaryLines = [ @@ -306,18 +327,17 @@ export function renderToday(stats: UsageStats, width: number): string { let output = renderBox(summaryLines, width, "Today"); - const days = Array.from(getDayStats(stats.entries).values()).slice(0, 7); - if (days.length > 1) { - const dayLines = days.slice(1).map((d) => `${d.date}: ${formatReqs(d.requests)} req, ${formatCost(d.satsCost)} sats, ${formatNumber(d.totalTokens)} tokens`); + if (recentDays.length > 0) { + const dayLines = recentDays.map((d) => `${d.date}: ${formatReqs(d.requests)} req, ${formatCost(d.satsCost)} sats, ${formatNumber(d.totalTokens)} tokens`); output += "\n" + renderBox(dayLines, width, "Recent Days"); } const hourLines: string[] = []; - const maxHourCost = Math.max(...Array.from(hourly.values()).map((h) => h.satsCost), 1); + const maxHourCost = Math.max(...Array.from(hourlyMap.values()).map((h) => h.satsCost), 1); const totalTodayCost = Math.max(todayStats.satsCost, 1); const hourLabels: string[] = []; for (let h = currentHour; h >= 0; h--) { - const hStat = hourly.get(h); + const hStat = hourlyMap.get(h); const reqs = hStat?.requests || 0; const cost = hStat?.satsCost || 0; hourLabels.push(`${h.toString().padStart(2, "0")}:00 (${formatReqs(reqs)} req, ${formatCost(cost)} sats) `); @@ -325,7 +345,7 @@ export function renderToday(stats: UsageStats, width: number): string { const maxHourLabel = Math.max(...hourLabels.map((l) => l.length)); startBarSection("hourly", maxHourLabel); for (let i = currentHour; i >= 0; i--) { - const hStat = hourly.get(i); + const hStat = hourlyMap.get(i); const reqs = hStat?.requests || 0; const cost = hStat?.satsCost || 0; hourLines.push(renderBarChart( @@ -346,7 +366,7 @@ export function renderToday(stats: UsageStats, width: number): string { } export function renderModels(stats: UsageStats, width: number): string { - const modelStats = getModelStats(stats.entries); + const modelStats = stats.summary?.models ?? getModelStats(stats.entries); if (modelStats.length === 0) return renderBox(["No model data available"], width, "Models"); // Use totalSatsCost (all-time) for percentage calculations to match header @@ -373,7 +393,7 @@ export function renderModels(stats: UsageStats, width: number): string { } export function renderProviders(stats: UsageStats, width: number): string { - const providerStats = getProviderStats(stats.entries); + const providerStats = stats.summary?.providers ?? getProviderStats(stats.entries); if (providerStats.length === 0) return renderBox(["No provider data available"], width, "Providers"); const lines: string[] = []; @@ -389,8 +409,8 @@ export function renderProviders(stats: UsageStats, width: number): string { } export function renderTokens(stats: UsageStats, width: number): string { - const totals = getTotals(stats.entries); - const modelStats = getModelStats(stats.entries); + const totals = stats.summary?.totals ?? getTotals(stats.entries); + const modelStats = stats.summary?.models ?? getModelStats(stats.entries); const summaryLines = [ `${COLORS.bold}Total Prompt Tokens:${COLORS.reset} ${formatNumber(totals.promptTokens)}`, `${COLORS.bold}Total Completion Tokens:${COLORS.reset} ${formatNumber(totals.completionTokens)}`, @@ -410,23 +430,25 @@ export function renderTokens(stats: UsageStats, width: number): string { output += "\n" + renderBox(tokenLines, width, "Tokens by Model"); } - const sizeBuckets = { - tiny: { min: 0, max: 1000, count: 0, cost: 0 }, - small: { min: 1000, max: 10000, count: 0, cost: 0 }, - medium: { min: 10000, max: 50000, count: 0, cost: 0 }, - large: { min: 50000, max: 100000, count: 0, cost: 0 }, - huge: { min: 100000, max: Infinity, count: 0, cost: 0 }, - }; - - for (const entry of stats.entries) { - for (const bucket of Object.values(sizeBuckets)) { - if (entry.totalTokens >= bucket.min && entry.totalTokens < bucket.max) { - bucket.count++; - bucket.cost += entry.satsCost; - break; + const sizeBuckets = stats.summary?.sizeBuckets ?? (() => { + const b = { + tiny: { min: 0, max: 1000, count: 0, cost: 0 }, + small: { min: 1000, max: 10000, count: 0, cost: 0 }, + medium: { min: 10000, max: 50000, count: 0, cost: 0 }, + large: { min: 50000, max: 100000, count: 0, cost: 0 }, + huge: { min: 100000, max: Infinity, count: 0, cost: 0 }, + }; + for (const entry of stats.entries) { + for (const bucket of Object.values(b)) { + if (entry.totalTokens >= bucket.min && entry.totalTokens < bucket.max) { + bucket.count++; + bucket.cost += entry.satsCost; + break; + } } } - } + return b; + })(); const sizeLines = Object.entries(sizeBuckets).map(([name, bucket]) => `${name.padEnd(6)}: ${formatReqs(bucket.count).padStart(5)} reqs, ${formatCost(bucket.cost)} sats`); output += "\n" + renderBox(sizeLines, width, "Request Size Distribution"); @@ -434,7 +456,7 @@ export function renderTokens(stats: UsageStats, width: number): string { } export function renderClients(stats: UsageStats, width: number): string { - const clientStats = getClientStats(stats.entries); + const clientStats = stats.summary?.clients ?? getClientStats(stats.entries); if (clientStats.length === 0) return renderBox(["No client data available (API key auth not used)"], width, "Client Breakdown"); // Use totalSatsCost (all-time) for percentage calculations to match header @@ -478,31 +500,43 @@ export function renderClients(stats: UsageStats, width: number): string { endBarSection("client-detail"); let output = renderBox(lines, width, "Client Breakdown"); - const clientModelMap = new Map>(); - - for (const entry of stats.entries) { - const client = entry.client || "unknown"; - const model = entry.modelId; - if (!clientModelMap.has(client)) clientModelMap.set(client, new Map()); - const modelMap = clientModelMap.get(client)!; - const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 }; - modelMap.set(model, { - requests: existing.requests + 1, - satsCost: existing.satsCost + entry.satsCost, - tokens: existing.tokens + entry.totalTokens, - }); - } - const clientModelLines: string[] = []; - for (const topClient of clientStats.slice(0, 3)) { - const modelMap = clientModelMap.get(topClient.client); - if (!modelMap) continue; - const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5); - clientModelLines.push(`${COLORS.bold}${topClient.client}${COLORS.reset} (${formatReqs(topClient.requests)} reqs, ${formatCost(topClient.satsCost)} sats)`); - for (const [model, data] of models) { - clientModelLines.push(` ${(MODEL_COLORS[model] || MODEL_COLORS.default)}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`); + + if (stats.summary) { + // Use pre-aggregated topModels from summary + for (const topClient of stats.summary.clients.slice(0, 3)) { + if (topClient.topModels.length === 0) continue; + clientModelLines.push(`${COLORS.bold}${topClient.client}${COLORS.reset} (${formatReqs(topClient.requests)} reqs, ${formatCost(topClient.satsCost)} sats)`); + for (const m of topClient.topModels) { + clientModelLines.push(` ${(MODEL_COLORS[m.modelId] || MODEL_COLORS.default)}${m.modelId.padEnd(18)}${COLORS.reset} ${formatNumber(m.totalTokens).padEnd(8)} tokens ${formatCost(m.satsCost)} sats`); + } + clientModelLines.push(""); + } + } else { + // Fallback: compute from raw entries + const clientModelMap = new Map>(); + for (const entry of stats.entries) { + const client = entry.client || "unknown"; + const model = entry.modelId; + if (!clientModelMap.has(client)) clientModelMap.set(client, new Map()); + const modelMap = clientModelMap.get(client)!; + const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 }; + modelMap.set(model, { + requests: existing.requests + 1, + satsCost: existing.satsCost + entry.satsCost, + tokens: existing.tokens + entry.totalTokens, + }); + } + for (const topClient of clientStats.slice(0, 3)) { + const modelMap = clientModelMap.get(topClient.client); + if (!modelMap) continue; + const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5); + clientModelLines.push(`${COLORS.bold}${topClient.client}${COLORS.reset} (${formatReqs(topClient.requests)} reqs, ${formatCost(topClient.satsCost)} sats)`); + for (const [model, data] of models) { + clientModelLines.push(` ${(MODEL_COLORS[model] || MODEL_COLORS.default)}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`); + } + clientModelLines.push(""); } - clientModelLines.push(""); } if (clientModelLines.length > 0) { @@ -512,7 +546,7 @@ export function renderClients(stats: UsageStats, width: number): string { } export function renderNpubs(stats: UsageStats, clients: ClientInfo[], width: number): string { - const npubStats = getNpubStats(stats.entries, clients); + const npubStats = stats.summary?.npubs ?? getNpubStats(stats.entries, clients); if (npubStats.length === 0) return renderBox(["No npub data available"], width, "Npub Breakdown"); const totalCost = stats.totalSatsCost; @@ -557,38 +591,48 @@ export function renderNpubs(stats: UsageStats, clients: ClientInfo[], width: num endBarSection("npub-detail"); let output = renderBox(lines, width, "Npub Breakdown"); - - // Top models per npub (same pattern as clients tab) - const npubModelMap = new Map>(); - const clientNpubMap = new Map(); - for (const c of clients) { - if (c.ownerNpub) clientNpubMap.set(c.clientId, c.ownerNpub); - } - - for (const entry of stats.entries) { - const npub = clientNpubMap.get(entry.client || ""); - if (!npub) continue; - const model = entry.modelId; - if (!npubModelMap.has(npub)) npubModelMap.set(npub, new Map()); - const modelMap = npubModelMap.get(npub)!; - const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 }; - modelMap.set(model, { - requests: existing.requests + 1, - satsCost: existing.satsCost + entry.satsCost, - tokens: existing.tokens + entry.totalTokens, - }); - } - const npubModelLines: string[] = []; - for (const topNpub of npubStats.slice(0, 5)) { - const modelMap = npubModelMap.get(topNpub.npub); - if (!modelMap) continue; - const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5); - npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`); - for (const [model, data] of models) { - npubModelLines.push(` ${(MODEL_COLORS[model] || MODEL_COLORS.default)}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`); + + if (stats.summary) { + // Use pre-aggregated topModels from summary + for (const topNpub of stats.summary.npubs.slice(0, 5)) { + if (topNpub.topModels.length === 0) continue; + npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`); + for (const m of topNpub.topModels) { + npubModelLines.push(` ${(MODEL_COLORS[m.modelId] || MODEL_COLORS.default)}${m.modelId.padEnd(18)}${COLORS.reset} ${formatNumber(m.totalTokens).padEnd(8)} tokens ${formatCost(m.satsCost)} sats`); + } + npubModelLines.push(""); + } + } else { + // Fallback: compute from raw entries + const npubModelMap = new Map>(); + const clientNpubMap = new Map(); + for (const c of clients) { + if (c.ownerNpub) clientNpubMap.set(c.clientId, c.ownerNpub); + } + for (const entry of stats.entries) { + const npub = clientNpubMap.get(entry.client || ""); + if (!npub) continue; + const model = entry.modelId; + if (!npubModelMap.has(npub)) npubModelMap.set(npub, new Map()); + const modelMap = npubModelMap.get(npub)!; + const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 }; + modelMap.set(model, { + requests: existing.requests + 1, + satsCost: existing.satsCost + entry.satsCost, + tokens: existing.tokens + entry.totalTokens, + }); + } + for (const topNpub of npubStats.slice(0, 5)) { + const modelMap = npubModelMap.get(topNpub.npub); + if (!modelMap) continue; + const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5); + npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`); + for (const [model, data] of models) { + npubModelLines.push(` ${(MODEL_COLORS[model] || MODEL_COLORS.default)}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`); + } + npubModelLines.push(""); } - npubModelLines.push(""); } if (npubModelLines.length > 0) { diff --git a/src/tui/usage/types.ts b/src/tui/usage/types.ts index 7e204bd..d49476d 100644 --- a/src/tui/usage/types.ts +++ b/src/tui/usage/types.ts @@ -1,4 +1,7 @@ import type { UsageTrackingEntry } from "../../daemon/types.ts"; +import type { UsageSummary } from "../../daemon/http/usage-summary.ts"; + +export type { UsageSummary }; export interface UsageStats { entries: UsageTrackingEntry[]; @@ -6,6 +9,8 @@ export interface UsageStats { totalSatsCost: number; recentSatsCost: number; limit: number; + /** Present when data was fetched via /usage/summary (server-aggregated path). */ + summary?: UsageSummary; } export interface DayStats { From f0978f22c1b718bd5a50b1f9f7e278c79e95b6c3 Mon Sep 17 00:00:00 2001 From: Bilthon Date: Tue, 2 Jun 2026 14:53:38 -0500 Subject: [PATCH 05/42] test: cover /usage/summary builder --- src/daemon/http/usage-summary.test.ts | 257 ++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 src/daemon/http/usage-summary.test.ts diff --git a/src/daemon/http/usage-summary.test.ts b/src/daemon/http/usage-summary.test.ts new file mode 100644 index 0000000..08af1a8 --- /dev/null +++ b/src/daemon/http/usage-summary.test.ts @@ -0,0 +1,257 @@ +import { describe, it, expect, beforeEach } from "bun:test"; +import { createMemoryUsageTrackingDriver } from "@routstr/sdk/storage"; +import type { UsageTrackingEntry } from "@routstr/sdk/storage"; +import type { ClientEntry } from "../../utils/clients"; +import { getUsageSummary } from "./usage-summary"; + +// ─── Test fixtures ──────────────────────────────────────────────────────────── +// +// All timestamps fall within 30 days of 2026-06-02 (today as of this writing). +// tz = 300 (UTC-5 / EST). After shifting by -300min, UTC dates become: +// d1: 2026-05-20T10:00Z → shifted 2026-05-20T05:00Z → local day "2026-05-20", hour 05 +// d2: 2026-05-21T03:00Z → shifted 2026-05-20T22:00Z → local day "2026-05-20", hour 22 +// d3: 2026-05-21T06:00Z → shifted 2026-05-21T01:00Z → local day "2026-05-21", hour 01 + +const D1 = new Date("2026-05-20T10:00:00Z").getTime(); // local 2026-05-20, hour 05 +const D2 = new Date("2026-05-21T03:00:00Z").getTime(); // local 2026-05-20, hour 22 +const D3 = new Date("2026-05-21T06:00:00Z").getTime(); // local 2026-05-21, hour 01 + +const BASE_ENTRY: Omit = { + baseUrl: "https://api.openai.com/", + requestId: "req-1", + cost: 0.01, + satsCost: 10, + promptTokens: 100, + completionTokens: 50, +}; + +function makeEntry( + id: string, + overrides: Partial & Pick, +): UsageTrackingEntry { + return { + ...BASE_ENTRY, + ...overrides, + id, + }; +} + +// Seed entries: +// - 2 entries for model-a, client-1 (owned by npub1) +// - 1 entry for model-b, client-2 (no npub) +// - 1 entry for model-a, no client (null client) +// - totalTokens spread across tiny (< 1000) and small (1000-10000) buckets + +const ENTRIES: UsageTrackingEntry[] = [ + // model-a, client-1, day 2026-05-20 (D1), totalTokens=500 (tiny) + makeEntry("e1", { + id: "e1", + timestamp: D1, + modelId: "model-a", + client: "client-1", + totalTokens: 500, + promptTokens: 350, + completionTokens: 150, + satsCost: 10, + cost: 0.01, + }), + // model-a, client-1, day 2026-05-20 (D2), totalTokens=2000 (small) + makeEntry("e2", { + id: "e2", + timestamp: D2, + modelId: "model-a", + client: "client-1", + totalTokens: 2000, + promptTokens: 1400, + completionTokens: 600, + satsCost: 20, + cost: 0.02, + }), + // model-b, client-2, day 2026-05-21 (D3), totalTokens=800 (tiny) + makeEntry("e3", { + id: "e3", + timestamp: D3, + modelId: "model-b", + client: "client-2", + totalTokens: 800, + promptTokens: 600, + completionTokens: 200, + satsCost: 8, + cost: 0.008, + }), + // model-a, no client, day 2026-05-21 (D3), totalTokens=5000 (small) + makeEntry("e4", { + id: "e4", + timestamp: D3 + 1000, + modelId: "model-a", + client: undefined, + totalTokens: 5000, + promptTokens: 3500, + completionTokens: 1500, + satsCost: 50, + cost: 0.05, + }), +]; + +const CLIENTS: ClientEntry[] = [ + { + clientId: "client-1", + name: "Client One", + apiKey: "sk-client1", + createdAt: D1 - 86400000, + ownerNpub: "npub1abc", + }, + { + clientId: "client-2", + name: "Client Two", + apiKey: "sk-client2", + createdAt: D1 - 86400000, + // no ownerNpub + }, +]; + +const TZ = 300; // UTC-5 / EST + +describe("getUsageSummary", () => { + let driver: ReturnType; + + beforeEach(async () => { + // Reset module-level cache between tests by creating a fresh driver with a + // distinct entry count each time — the cache key changes. + driver = createMemoryUsageTrackingDriver(ENTRIES); + }); + + it("returns correct totals", async () => { + const summary = await getUsageSummary(driver, CLIENTS, TZ); + + expect(summary.totals.requests).toBe(4); + expect(summary.totals.totalTokens).toBe(500 + 2000 + 800 + 5000); + expect(summary.totals.promptTokens).toBe(350 + 1400 + 600 + 3500); + expect(summary.totals.completionTokens).toBe(150 + 600 + 200 + 1500); + expect(summary.totals.satsCost).toBe(10 + 20 + 8 + 50); + expect(summary.totals.cost).toBeCloseTo(0.01 + 0.02 + 0.008 + 0.05, 5); + }); + + it("returns models sorted desc by satsCost", async () => { + const summary = await getUsageSummary(driver, CLIENTS, TZ); + const { models } = summary; + + // model-a: 3 entries (e1, e2, e4) total satsCost = 10+20+50 = 80 + // model-b: 1 entry (e3) satsCost = 8 + expect(models).toHaveLength(2); + expect(models[0]!.modelId).toBe("model-a"); + expect(models[0]!.satsCost).toBe(80); + expect(models[0]!.requests).toBe(3); + expect(models[1]!.modelId).toBe("model-b"); + expect(models[1]!.satsCost).toBe(8); + }); + + it("returns days most-recent-first with correct tz-bucketing", async () => { + const summary = await getUsageSummary(driver, CLIENTS, TZ); + const { days } = summary; + + // D1 (10:00Z) → local 2026-05-20, D2 (03:00Z) → local 2026-05-20, D3 (06:00Z) → local 2026-05-21 + // So: day 2026-05-20 has e1+e2, day 2026-05-21 has e3+e4 + expect(days.length).toBeGreaterThanOrEqual(2); + + // Most-recent-first: 2026-05-21 first + expect(days[0]!.date).toBe("2026-05-21"); + expect(days[0]!.requests).toBe(2); // e3, e4 + expect(days[0]!.satsCost).toBe(8 + 50); + + expect(days[1]!.date).toBe("2026-05-20"); + expect(days[1]!.requests).toBe(2); // e1, e2 + expect(days[1]!.satsCost).toBe(10 + 20); + }); + + it("returns empty hoursToday (test entries are not today)", async () => { + const summary = await getUsageSummary(driver, CLIENTS, TZ); + // Test entries are in May 2026, not today (June 2026) + expect(summary.hoursToday).toHaveLength(0); + }); + + it("returns correct sizeBuckets", async () => { + const summary = await getUsageSummary(driver, CLIENTS, TZ); + const { sizeBuckets } = summary; + + // tiny [0, 1000): e1 (500) + e3 (800) = 2 entries + expect(sizeBuckets.tiny.count).toBe(2); + expect(sizeBuckets.tiny.cost).toBe(10 + 8); + + // small [1000, 10000): e2 (2000) + e4 (5000) = 2 entries + expect(sizeBuckets.small.count).toBe(2); + expect(sizeBuckets.small.cost).toBe(20 + 50); + + // medium, large, huge should be 0 + expect(sizeBuckets.medium.count).toBe(0); + expect(sizeBuckets.large.count).toBe(0); + expect(sizeBuckets.huge.count).toBe(0); + }); + + it("returns per-client topModels for top non-null clients", async () => { + const summary = await getUsageSummary(driver, CLIENTS, TZ); + const { clients } = summary; + + // client-1: e1 + e2 (model-a), satsCost = 30 + // client-2: e3 (model-b), satsCost = 8 + // unknown (null client): e4 (model-a), satsCost = 50 + // Sorted desc by satsCost: unknown (50), client-1 (30), client-2 (8) + + const c1 = clients.find((c) => c.client === "client-1"); + expect(c1).toBeDefined(); + expect(c1!.requests).toBe(2); + expect(c1!.satsCost).toBe(30); + // client-1 is in top 3 non-null clients, should have topModels + expect(c1!.topModels.length).toBeGreaterThan(0); + expect(c1!.topModels[0]!.modelId).toBe("model-a"); + + const c2 = clients.find((c) => c.client === "client-2"); + expect(c2).toBeDefined(); + expect(c2!.requests).toBe(1); + expect(c2!.satsCost).toBe(8); + + // unknown client (null group) should have empty topModels + const unknown = clients.find((c) => c.client === "unknown"); + expect(unknown).toBeDefined(); + expect(unknown!.topModels).toHaveLength(0); + }); + + it("folds client rows into npubs correctly", async () => { + const summary = await getUsageSummary(driver, CLIENTS, TZ); + const { npubs } = summary; + + // Only client-1 has ownerNpub = "npub1abc" + // npub1abc: e1 + e2 = requests 2, satsCost 30 + expect(npubs).toHaveLength(1); + expect(npubs[0]!.npub).toBe("npub1abc"); + expect(npubs[0]!.requests).toBe(2); + expect(npubs[0]!.satsCost).toBe(30); + // Top models for npub1abc (clients: ["client-1"]): only model-a + expect(npubs[0]!.topModels.length).toBeGreaterThan(0); + expect(npubs[0]!.topModels[0]!.modelId).toBe("model-a"); + }); + + it("returns recent entries (up to 50)", async () => { + const summary = await getUsageSummary(driver, CLIENTS, TZ); + expect(summary.recent).toHaveLength(4); // only 4 entries in driver + // Should be sorted desc by timestamp + expect(summary.recent[0]!.timestamp).toBeGreaterThanOrEqual(summary.recent[1]!.timestamp); + }); + + it("returns providers correctly", async () => { + const summary = await getUsageSummary(driver, CLIENTS, TZ); + const { providers } = summary; + + // All entries use the same baseUrl + expect(providers).toHaveLength(1); + expect(providers[0]!.baseUrl).toBe("https://api.openai.com/"); + expect(providers[0]!.requests).toBe(4); + }); + + it("caches results for same key within TTL", async () => { + const summary1 = await getUsageSummary(driver, CLIENTS, TZ); + const summary2 = await getUsageSummary(driver, CLIENTS, TZ); + // Same object reference means it was served from cache + expect(summary1).toBe(summary2); + }); +}); From b60b810dc8cb7dfc4fee534e220a1c9f1f534a3a Mon Sep 17 00:00:00 2001 From: Bilthon Date: Tue, 2 Jun 2026 15:26:14 -0500 Subject: [PATCH 06/42] fix(monitor): match summary today bucket to local day --- src/tui/usage/render.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/tui/usage/render.ts b/src/tui/usage/render.ts index 0a84823..7061499 100644 --- a/src/tui/usage/render.ts +++ b/src/tui/usage/render.ts @@ -283,7 +283,12 @@ export function renderOverview(stats: UsageStats, balance: BalanceInfo | null, s export function renderToday(stats: UsageStats, width: number): string { const currentHour = new Date().getHours(); - const todayDateStr = formatDate(Date.now()); + // Build today's date string from LOCAL date components so it matches the + // server's tz-bucketed day keys (which use the client's tzOffsetMinutes). + // formatDate() returns a UTC date via toISOString() and would mismatch for + // non-UTC users. + const _now = new Date(); + const todayDateStr = `${_now.getFullYear()}-${String(_now.getMonth() + 1).padStart(2, "0")}-${String(_now.getDate()).padStart(2, "0")}`; let todayStats: { date: string; requests: number; satsCost: number; promptTokens: number; completionTokens: number; totalTokens: number }; let recentDays: Array<{ date: string; requests: number; satsCost: number; totalTokens: number; promptTokens: number; completionTokens: number }>; From c08f64941e141066b9ccf5052d01030d657fb950 Mon Sep 17 00:00:00 2001 From: Bilthon Date: Tue, 2 Jun 2026 15:26:43 -0500 Subject: [PATCH 07/42] fix(monitor): align top-models-per-client across summary and legacy --- src/tui/usage/render.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/tui/usage/render.ts b/src/tui/usage/render.ts index 7061499..2da00bc 100644 --- a/src/tui/usage/render.ts +++ b/src/tui/usage/render.ts @@ -508,8 +508,9 @@ export function renderClients(stats: UsageStats, width: number): string { const clientModelLines: string[] = []; if (stats.summary) { - // Use pre-aggregated topModels from summary - for (const topClient of stats.summary.clients.slice(0, 3)) { + // Use pre-aggregated topModels from summary; exclude the "unknown" bucket + // (null client rows have no meaningful model attribution to display here). + for (const topClient of stats.summary.clients.filter((c) => c.client !== "unknown").slice(0, 3)) { if (topClient.topModels.length === 0) continue; clientModelLines.push(`${COLORS.bold}${topClient.client}${COLORS.reset} (${formatReqs(topClient.requests)} reqs, ${formatCost(topClient.satsCost)} sats)`); for (const m of topClient.topModels) { @@ -518,7 +519,8 @@ export function renderClients(stats: UsageStats, width: number): string { clientModelLines.push(""); } } else { - // Fallback: compute from raw entries + // Fallback: compute from raw entries; exclude the "unknown" bucket to match + // the summary path. const clientModelMap = new Map>(); for (const entry of stats.entries) { const client = entry.client || "unknown"; @@ -532,7 +534,7 @@ export function renderClients(stats: UsageStats, width: number): string { tokens: existing.tokens + entry.totalTokens, }); } - for (const topClient of clientStats.slice(0, 3)) { + for (const topClient of clientStats.filter((c) => c.client !== "unknown").slice(0, 3)) { const modelMap = clientModelMap.get(topClient.client); if (!modelMap) continue; const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5); From bc8611ba7f4bf467db76194076b89e579ef09abc Mon Sep 17 00:00:00 2001 From: Bilthon Date: Tue, 2 Jun 2026 15:26:55 -0500 Subject: [PATCH 08/42] fix(daemon): include client identity in usage-summary cache key --- src/daemon/http/usage-summary.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/daemon/http/usage-summary.ts b/src/daemon/http/usage-summary.ts index ccd3023..5e164e6 100644 --- a/src/daemon/http/usage-summary.ts +++ b/src/daemon/http/usage-summary.ts @@ -129,9 +129,12 @@ export async function getUsageSummary( clients: ClientEntry[], tzOffsetMinutes: number, ): Promise { - // Cheap cache key: total row count + clients length + tz + // Cache key: total row count + per-client identity (id + ownerNpub) + tz. + // Including ownerNpub ensures a client assignment change invalidates the cache + // even though the row count doesn't change. const count = await driver.count(); - const cacheKey = `${count}:${clients.length}:${tzOffsetMinutes}`; + const clientIdentity = clients.map((c) => `${c.clientId}:${c.ownerNpub ?? ""}`).join(","); + const cacheKey = `${count}:${clientIdentity}:${tzOffsetMinutes}`; const now = Date.now(); if ( From c47145137d7a26e05915021605eb814e90df2f73 Mon Sep 17 00:00:00 2001 From: Bilthon Date: Tue, 2 Jun 2026 15:27:19 -0500 Subject: [PATCH 09/42] fix(test): export reset hook and call it in beforeEach for cache isolation --- src/daemon/http/usage-summary.test.ts | 8 +++++--- src/daemon/http/usage-summary.ts | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/daemon/http/usage-summary.test.ts b/src/daemon/http/usage-summary.test.ts index 08af1a8..59f97df 100644 --- a/src/daemon/http/usage-summary.test.ts +++ b/src/daemon/http/usage-summary.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from "bun:test"; import { createMemoryUsageTrackingDriver } from "@routstr/sdk/storage"; import type { UsageTrackingEntry } from "@routstr/sdk/storage"; import type { ClientEntry } from "../../utils/clients"; -import { getUsageSummary } from "./usage-summary"; +import { getUsageSummary, __resetUsageSummaryCacheForTest } from "./usage-summary"; // ─── Test fixtures ──────────────────────────────────────────────────────────── // @@ -116,8 +116,10 @@ describe("getUsageSummary", () => { let driver: ReturnType; beforeEach(async () => { - // Reset module-level cache between tests by creating a fresh driver with a - // distinct entry count each time — the cache key changes. + // Reset the module-level memo cache explicitly so each test starts cold. + // Without this, tests 2-10 would share the cached object from test 1 + // (all use the same fixtures → identical cache key → same TTL window). + __resetUsageSummaryCacheForTest(); driver = createMemoryUsageTrackingDriver(ENTRIES); }); diff --git a/src/daemon/http/usage-summary.ts b/src/daemon/http/usage-summary.ts index 5e164e6..a17460f 100644 --- a/src/daemon/http/usage-summary.ts +++ b/src/daemon/http/usage-summary.ts @@ -122,6 +122,11 @@ interface CacheEntry { let _cache: CacheEntry | null = null; const CACHE_TTL_MS = 60_000; +/** Clears the module-level memo cache. Intended for use in unit tests only. */ +export function __resetUsageSummaryCacheForTest(): void { + _cache = null; +} + // ─── Main builder ───────────────────────────────────────────────────────────── export async function getUsageSummary( From 6b746debede605b5a3518bf9848d98a7895b16f1 Mon Sep 17 00:00:00 2001 From: Bilthon Date: Tue, 2 Jun 2026 22:03:56 -0500 Subject: [PATCH 10/42] fix(monitor): use all-time summary totals in overview and exclude today from recent days --- src/tui/usage/render.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/tui/usage/render.ts b/src/tui/usage/render.ts index 2da00bc..5d6a365 100644 --- a/src/tui/usage/render.ts +++ b/src/tui/usage/render.ts @@ -120,8 +120,10 @@ export function renderBarChart( export function renderOverview(stats: UsageStats, balance: BalanceInfo | null, status: StatusInfo | null, width: number): string { - // Use the server-calculated totals (all entries) instead of summing limited entries - const totals = getTotals(stats.entries); + // Token totals must come from the all-time summary (when present) to stay on + // the same horizon as totalRequests/totalSatsCost below — stats.entries holds + // only the latest 50 in summary mode. Fall back to entries for the legacy path. + const totals = stats.summary?.totals ?? getTotals(stats.entries); const totalRequests = stats.totalEntries; // Use server's total count, not entries.length const totalVisibleCost = stats.totalSatsCost; // <-- Use server's total, not client-side sum of limited entries const avgCost = totalRequests > 0 ? totalVisibleCost / totalRequests : 0; @@ -302,7 +304,9 @@ export function renderToday(stats: UsageStats, width: number): string { 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 }; - recentDays = days.slice(1, 7); + // Exclude today by date (it has its own box) rather than by position — + // days[0] is only today when there was activity today. + recentDays = days.filter((d) => d.date !== todayDateStr).slice(0, 6); hourlyMap = new Map(hoursToday.map((h) => [h.hour, { requests: h.requests, satsCost: h.satsCost }])); } else { // Fallback: compute from raw entries From bc7ac9ff01ce56f7865bbb1b58d4b24025865c5c Mon Sep 17 00:00:00 2001 From: Bilthon Date: Wed, 3 Jun 2026 11:39:16 -0500 Subject: [PATCH 11/42] fix(daemon): import bun ModelManager for event persistence --- src/daemon/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 0c1b16d..c10e6f4 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -1,11 +1,15 @@ import { createServer } from "http"; import { existsSync } from "fs"; import { - ModelManager, ProviderManager, createStorageAdapterFromStore, createSdkStore, } from "@routstr/sdk"; +// ModelManager must come from the bun entrypoint so persistent Nostr event +// storage (eventStoreDbPath) gets its SQLite-backed factory. The default +// "@routstr/sdk" export is browser-safe and throws without that factory +// (SDK 0.3.7+ browser-safe entrypoint split). +import { ModelManager } from "@routstr/sdk/bun"; import type { SdkLogger } from "@routstr/sdk"; import { CONFIG_DIR, DB_PATH, SOCKET_PATH, PID_FILE } from "../utils/config"; import { logger } from "../utils/logger"; From a637ecf8041fd4f7f3c1f3d03b4e09087ef15aad Mon Sep 17 00:00:00 2001 From: Bilthon Date: Thu, 4 Jun 2026 00:46:52 -0500 Subject: [PATCH 12/42] bench: add in-process /usage/summary vs legacy benchmark --- scripts/bench-usage.ts | 377 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 scripts/bench-usage.ts diff --git a/scripts/bench-usage.ts b/scripts/bench-usage.ts new file mode 100644 index 0000000..cc8a540 --- /dev/null +++ b/scripts/bench-usage.ts @@ -0,0 +1,377 @@ +/** + * bench-usage.ts — In-process, no-network benchmark (Option B). + * + * Compares the NEW server-aggregated summary path against the LEGACY + * "fetch-everything-then-re-aggregate" path, in terms of TIME, highlighting + * the daemon-side cache that lives in `getUsageSummary`. + * + * It deliberately SKIPS the HTTP layer. In production: + * - `fetchUsageSummary()` HTTP-calls the daemon, which runs `getUsageSummary()`. + * - `fetchUsage(N)` HTTP-calls the daemon, which runs `driver.list({limit:N})`. + * The real localhost socket round-trip (~1-3 ms) is roughly CONSTANT for both + * paths, so excluding it isolates the actual computational difference — which is + * the whole point. See the takeaway printed at the end. + * + * Run from the repo root: bun scripts/bench-usage.ts [--json] + * + */ + +// @ts-ignore — bun:sqlite is a Bun built-in module (matches storage/bun.ts). +import { Database } from "bun:sqlite"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { rmSync } from "node:fs"; + +import { createBunSqliteUsageTrackingDriver } from "@routstr/sdk/storage/bun"; +import { + getUsageSummary, + __resetUsageSummaryCacheForTest, +} from "../src/daemon/http/usage-summary.ts"; +import { + getTotals, + getModelStats, + getProviderStats, + getClientStats, + getDayStats, + getHourlyToday, + getNpubStats, +} from "../src/tui/usage/data.ts"; +import type { UsageTrackingEntry } from "../src/daemon/types.ts"; + +// ─── Config ────────────────────────────────────────────────────────────────── + +const SIZES = [100, 1000, 2500, 5000, 10000, 20000]; +const SAMPLES = 25; // median of 25, after a warmup +const STEADY_REFRESHES = 10; // the monitor's ~2s refresh loop +const TZ_OFFSET = 300; // synthetic UTC-5; fixed so results are reproducible +const DAY_SPREAD = 45; // spread timestamps over ~45 days + +const jsonMode = process.argv.includes("--json"); + +// ─── Synthetic, sanitized templates (no real DB, no real identifiers) ───────── + +interface Template { + modelId: string; + baseUrl: string; + client: string; + cost: number; + satsCost: number; + promptTokens: number; + completionTokens: number; +} + +const PROVIDER_A = "https://api.example-provider-a.com/"; +const PROVIDER_B = "https://api.example-provider-b.com/"; + +const TEMPLATES: Template[] = [ + { modelId: "minimax-m3", baseUrl: PROVIDER_A, client: "client-a", cost: 0.0021, satsCost: 4, promptTokens: 320, completionTokens: 180 }, + { modelId: "kimi-k2.6", baseUrl: PROVIDER_A, client: "client-b", cost: 0.0140, satsCost: 27, promptTokens: 5200, completionTokens: 2400 }, + { modelId: "deepseek-v4-flash", baseUrl: PROVIDER_B, client: "client-c", cost: 0.0008, satsCost: 1, promptTokens: 90, completionTokens: 60 }, + { modelId: "deepseek-v4-flash", baseUrl: PROVIDER_B, client: "client-a", cost: 0.0420, satsCost: 81, promptTokens: 24000, completionTokens: 13000 }, + { modelId: "kimi-k2.6", baseUrl: PROVIDER_B, client: "client-b", cost: 0.0065, satsCost: 12, promptTokens: 1800, completionTokens: 950 }, +]; + +// Clients array (shape works for both getUsageSummary's ClientEntry and +// data.ts's ClientInfo — both only read clientId / ownerNpub). ~half get a +// synthetic ownerNpub so the npub aggregation path is exercised. +const CLIENT_IDS = [...new Set(TEMPLATES.map((t) => t.client))]; +const clients = CLIENT_IDS.map((id, i) => ({ + clientId: id, + name: id, + apiKey: `apikey-${id}`, + createdAt: 0, + ownerNpub: i % 2 === 0 ? `npub1bench${id.replace(/[^a-z]/g, "")}` : undefined, +})); + +// ─── Row generation ──────────────────────────────────────────────────────────── + +function generateRows(n: number, now: number): UsageTrackingEntry[] { + const rows: UsageTrackingEntry[] = new Array(n); + const spreadMs = DAY_SPREAD * 86_400_000; + for (let i = 0; i < n; i++) { + const t = TEMPLATES[i % TEMPLATES.length]!; + // Spread timestamps backwards from `now` over DAY_SPREAD days, with a + // little jitter so multiple buckets/hours are populated. + const jitter = (i * 97) % 86_400_000; + const timestamp = now - Math.floor((i / n) * spreadMs) - jitter; + const promptTokens = t.promptTokens + (i % 17); + const completionTokens = t.completionTokens + (i % 13); + rows[i] = { + id: `bench-${i}`, + timestamp, + modelId: t.modelId, + baseUrl: t.baseUrl, + requestId: `req-${i}`, + cost: t.cost, + satsCost: t.satsCost, + promptTokens, + completionTokens, + totalTokens: promptTokens + completionTokens, + client: t.client, + sessionId: `sess-${i % 50}`, + tags: [], + }; + } + return rows; +} + +// ─── Seeding (raw transactional insert — fast) ──────────────────────────────── + +const TABLE = "usage_tracking"; + +function seedRawDb(dbPath: string, rows: UsageTrackingEntry[]): void { + const db = new Database(dbPath); + // Match the SDK schema exactly (copied from storage/bun.ts). + db.run(` + CREATE TABLE IF NOT EXISTS ${TABLE} ( + id TEXT PRIMARY KEY, + timestamp INTEGER NOT NULL, + model_id TEXT NOT NULL, + base_url TEXT NOT NULL, + request_id TEXT NOT NULL, + cost REAL NOT NULL, + sats_cost REAL NOT NULL, + prompt_tokens INTEGER NOT NULL, + completion_tokens INTEGER NOT NULL, + total_tokens INTEGER NOT NULL, + client TEXT, + session_id TEXT, + tags TEXT + ) + `); + db.run(`CREATE INDEX IF NOT EXISTS idx_${TABLE}_timestamp ON ${TABLE}(timestamp)`); + db.run(`CREATE INDEX IF NOT EXISTS idx_${TABLE}_model_id ON ${TABLE}(model_id)`); + db.run(`CREATE INDEX IF NOT EXISTS idx_${TABLE}_base_url ON ${TABLE}(base_url)`); + + const insert = db.query(` + INSERT OR REPLACE INTO ${TABLE} ( + id, timestamp, model_id, base_url, request_id, + cost, sats_cost, prompt_tokens, completion_tokens, total_tokens, + client, session_id, tags + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + const seed = db.transaction((batch: UsageTrackingEntry[]) => { + for (const r of batch) { + insert.run( + r.id, r.timestamp, r.modelId, r.baseUrl, r.requestId, + r.cost, r.satsCost, r.promptTokens, r.completionTokens, r.totalTokens, + r.client ?? null, r.sessionId ?? null, JSON.stringify(r.tags ?? []), + ); + } + }); + seed(rows); + db.close(); +} + +// ─── Timing helpers ──────────────────────────────────────────────────────────── + +async function timeMedian(fn: () => Promise, samples = SAMPLES): Promise { + await fn(); // warmup (discarded) + const times: number[] = []; + for (let i = 0; i < samples; i++) { + const start = performance.now(); + await fn(); + times.push(performance.now() - start); + } + times.sort((a, b) => a - b); + return times[Math.floor(times.length / 2)]!; +} + +function runLegacyAggregation(rows: UsageTrackingEntry[]): void { + // Mirror exactly what the monitor's render path does on raw entries. + getTotals(rows); + getModelStats(rows); + getProviderStats(rows); + getClientStats(rows); + getDayStats(rows); + getHourlyToday(rows); + getNpubStats(rows, clients); +} + +// ─── Bench per size ────────────────────────────────────────────────────────── + +interface SizeResult { + size: number; + summaryColdMs: number; + summaryWarmMs: number; + legacyMs: number; + summaryKB: number; + legacyKB: number; + reductionX: number; + steadySummaryMs: number; + steadyLegacyMs: number; + speedupX: number; +} + +async function benchSize(size: number): Promise { + const now = Date.now(); + const dbPath = join(tmpdir(), `routstrd-bench-${size}-${process.pid}-${Date.now()}.db`); + const rows = generateRows(size, now); + seedRawDb(dbPath, rows); + + const driver = await createBunSqliteUsageTrackingDriver({ dbPath }); + + try { + // ── summary cold: reset cache before every timed call ────────────────── + const summaryColdMs = await timeMedian(async () => { + __resetUsageSummaryCacheForTest(); + await getUsageSummary(driver, clients, TZ_OFFSET); + }); + + // ── summary warm: cache already populated (warmup call is itself a hit) ─ + __resetUsageSummaryCacheForTest(); + await getUsageSummary(driver, clients, TZ_OFFSET); // populate once + const summaryWarmMs = await timeMedian(async () => { + await getUsageSummary(driver, clients, TZ_OFFSET); + }); + + // ── legacy: list(limit:N) + full client-side re-aggregation, no cache ── + const legacyMs = await timeMedian(async () => { + const listed = await driver.list({ limit: size }); + runLegacyAggregation(listed); + }); + + // ── payload bytes ────────────────────────────────────────────────────── + __resetUsageSummaryCacheForTest(); + const summaryResult = await getUsageSummary(driver, clients, TZ_OFFSET); + const listedRows = await driver.list({ limit: size }); + const summaryBytes = JSON.stringify(summaryResult).length; + const legacyBytes = JSON.stringify(listedRows).length; + + // ── steady state (the 2s refresh loop, STEADY_REFRESHES iterations) ──── + // summary: 1 cold + (N-1) warm; legacy: N × full cost (no cache). + const steadySummaryMs = + summaryColdMs + (STEADY_REFRESHES - 1) * summaryWarmMs; + const steadyLegacyMs = STEADY_REFRESHES * legacyMs; + + return { + size, + summaryColdMs, + summaryWarmMs, + legacyMs, + summaryKB: summaryBytes / 1024, + legacyKB: legacyBytes / 1024, + reductionX: legacyBytes / summaryBytes, + steadySummaryMs, + steadyLegacyMs, + speedupX: steadyLegacyMs / steadySummaryMs, + }; + } finally { + // driver holds the only open handle now; closing the file by removing it. + try { + rmSync(dbPath, { force: true }); + rmSync(`${dbPath}-shm`, { force: true }); + rmSync(`${dbPath}-wal`, { force: true }); + } catch { /* ignore */ } + } +} + +// ─── Output ────────────────────────────────────────────────────────────────── + +function fmt(n: number, dp = 2): string { + return n.toFixed(dp); +} + +function pad(s: string, width: number): string { + return s.padStart(width); +} + +function printTable(results: SizeResult[]): void { + console.log(""); + console.log("Routstr usage path benchmark — TIME (in-process, no network)"); + console.log("Daemon-side summary cache vs. legacy fetch-all-and-re-aggregate.\n"); + + // ── Payload table (per request) ────────────────────────────── + console.log("Payload per request:"); + const payHeader = [ + pad("size", 7), + pad("summary", 11), + pad("legacy", 11), + pad("reduction", 11), + ].join(" "); + console.log(payHeader); + console.log("-".repeat(payHeader.length)); + for (const r of results) { + console.log([ + pad(String(r.size), 7), + pad(fmt(r.summaryKB, 1) + " KB", 11), + pad(fmt(r.legacyKB, 1) + " KB", 11), + pad(fmt(r.reductionX, 1) + "x", 11), + ].join(" ")); + } + + // ── Time table (ms) ────────────────────────────────────────── + console.log(""); + console.log("Time (ms):"); + const timeHeader = [ + pad("size", 7), + pad("sum cold", 10), + pad("sum warm", 10), + pad("legacy", 10), + pad("steady sum", 12), + pad("steady leg", 12), + pad("speedup", 9), + ].join(" "); + console.log(timeHeader); + console.log("-".repeat(timeHeader.length)); + for (const r of results) { + console.log([ + pad(String(r.size), 7), + pad(fmt(r.summaryColdMs) + "ms", 10), + pad(fmt(r.summaryWarmMs) + "ms", 10), + pad(fmt(r.legacyMs) + "ms", 10), + pad(fmt(r.steadySummaryMs) + "ms", 12), + pad(fmt(r.steadyLegacyMs) + "ms", 12), + pad(fmt(r.speedupX, 1) + "x", 9), + ].join(" ")); + } + + console.log(""); + console.log("Legend (in-process; times = medians of " + SAMPLES + " samples, warmup discarded;"); + console.log("excludes the ~constant localhost HTTP round-trip both paths would add):"); + console.log(" size seeded usage rows in the DB"); + console.log(""); + console.log(" Payload table:"); + console.log(" summary JSON bytes of the /usage/summary response (aggregates + 50 recent rows)"); + console.log(" legacy JSON bytes of the /usage response (all rows)"); + console.log(" reduction legacy / summary"); + console.log(""); + console.log(" Time table:"); + console.log(" sum cold summary build, COLD cache (cache miss: ~15-query rebuild + assemble)"); + console.log(" sum warm summary from WARM cache (count() + return the memoized result)"); + console.log(" legacy list all rows + the client-side re-aggregation the monitor does (no cache)"); + console.log(" steady sum total ms the SUMMARY path spends over " + STEADY_REFRESHES + " refreshes of the monitor's ~2s loop,"); + console.log(" assuming data is unchanged across the window (cache stays valid):"); + console.log(" 1 cold build + " + (STEADY_REFRESHES - 1) + " warm cache hits = cold + " + (STEADY_REFRESHES - 1) + "xwarm. Since warm is ~0.01ms,"); + console.log(" this is in practice ~one cold build."); + console.log(" steady leg same " + STEADY_REFRESHES + " refreshes for LEGACY = legacy x" + STEADY_REFRESHES + " (no cache, full cost each time)"); + console.log(" speedup steady leg / steady sum"); +} + +// ─── Main ──────────────────────────────────────────────────────────────────── + +async function main(): Promise { + const results: SizeResult[] = []; + for (const size of SIZES) { + results.push(await benchSize(size)); + } + + if (jsonMode) { + console.log(JSON.stringify( + { + config: { sizes: SIZES, samples: SAMPLES, steadyRefreshes: STEADY_REFRESHES, tzOffset: TZ_OFFSET }, + note: "In-process; HTTP round-trip (~1-3ms localhost, ~constant for both) excluded.", + results, + }, + null, + 2, + )); + } else { + printTable(results); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); From d86a231ed3740e236c7a23f1fcfa56cb33f338a6 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:13:06 +0800 Subject: [PATCH 13/42] fix: use Bun-safe SDK entrypoints for ModelManager and storage - Import ModelManager from @routstr/sdk/bun instead of @routstr/sdk - Import storage helpers from @routstr/sdk/storage/bun instead of @routstr/sdk/storage - Fix createBunSqliteUsageTrackingDriver call: async and no longer needs manual bun:sqlite import (now handled internally by the sdk entrypoint) - Verified sharded discovery adapter setup matches SDK pattern in scripts/routstr-daemon.ts at 6077aa7 --- bun.lock | 200 +++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- src/daemon/index.ts | 8 +- 3 files changed, 202 insertions(+), 8 deletions(-) diff --git a/bun.lock b/bun.lock index 45795d7..eab41b4 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "name": "routstrd", "dependencies": { "@cashu/cashu-ts": "^4.3.0", - "@routstr/sdk": "^0.3.9", + "@routstr/sdk": "file:../routstr-sdk", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", @@ -33,6 +33,66 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@libsql/client": ["@libsql/client@0.15.15", "", { "dependencies": { "@libsql/core": "^0.15.14", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w=="], "@libsql/core": ["@libsql/core@0.15.15", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA=="], @@ -71,7 +131,57 @@ "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], - "@routstr/sdk": ["@routstr/sdk@0.3.9", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "rxjs": "^7.8.1", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-bvn52jEYCcxOEVci1bGlbfH10LUmv2KErbFiXObp+hdnTg8ARthnY4DD6aCC5uJS71gKjRYWAFP16blMzs1aUA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.61.1", "", { "os": "android", "cpu": "arm" }, "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.61.1", "", { "os": "android", "cpu": "arm64" }, "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.61.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.61.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.61.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.61.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.61.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.61.1", "", { "os": "linux", "cpu": "arm" }, "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.61.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.61.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.61.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.61.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.61.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.61.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.61.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.61.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.61.1", "", { "os": "none", "cpu": "arm64" }, "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.61.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.61.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.61.1", "", { "os": "win32", "cpu": "x64" }, "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.61.1", "", { "os": "win32", "cpu": "x64" }, "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw=="], + + "@routstr/sdk": ["@routstr/sdk@file:../routstr-sdk", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "rxjs": "^7.8.1", "zustand": "^5.0.5" }, "devDependencies": { "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.0.0", "joycon": "^3.1.1", "tsup": "^8.5.1", "typescript": "^5.0.0" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }], "@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="], @@ -97,18 +207,26 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + "applesauce-common": ["applesauce-common@6.1.0", "", { "dependencies": { "@scure/base": "^1.2.4", "applesauce-core": "^6.1.0", "hash-sum": "^2.0.0", "light-bolt11-decoder": "^3.2.0", "nanoid": "^5.0.9", "rxjs": "^7.8.1" } }, "sha512-z5azXCIrSknGw57hzUN8F30Xd4clvLCfRQYM0asyyIQJtwxuEl0N/zrXX7RqdiJN/9yFCuLzBro2XoFezoeOMA=="], "applesauce-core": ["applesauce-core@5.2.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-aSuM6q6/Gs2FGUqytlHDjKZpSst2xKaT0vMXUQFWUctECNIxvwy6/hTDDInukMuI9mrQdjnO781ZJJgghI7RNw=="], @@ -131,8 +249,14 @@ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], @@ -143,6 +267,10 @@ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -161,20 +289,28 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], @@ -189,22 +325,36 @@ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], "libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="], "light-bolt11-decoder": ["light-bolt11-decoder@3.2.0", "", { "dependencies": { "@scure/base": "1.1.1" } }, "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + "nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="], "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], @@ -219,6 +369,8 @@ "nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], @@ -229,8 +381,20 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], @@ -243,10 +407,16 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "rollup": ["rollup@4.61.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.1", "@rollup/rollup-android-arm64": "4.61.1", "@rollup/rollup-darwin-arm64": "4.61.1", "@rollup/rollup-darwin-x64": "4.61.1", "@rollup/rollup-freebsd-arm64": "4.61.1", "@rollup/rollup-freebsd-x64": "4.61.1", "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", "@rollup/rollup-linux-arm-musleabihf": "4.61.1", "@rollup/rollup-linux-arm64-gnu": "4.61.1", "@rollup/rollup-linux-arm64-musl": "4.61.1", "@rollup/rollup-linux-loong64-gnu": "4.61.1", "@rollup/rollup-linux-loong64-musl": "4.61.1", "@rollup/rollup-linux-ppc64-gnu": "4.61.1", "@rollup/rollup-linux-ppc64-musl": "4.61.1", "@rollup/rollup-linux-riscv64-gnu": "4.61.1", "@rollup/rollup-linux-riscv64-musl": "4.61.1", "@rollup/rollup-linux-s390x-gnu": "4.61.1", "@rollup/rollup-linux-x64-gnu": "4.61.1", "@rollup/rollup-linux-x64-musl": "4.61.1", "@rollup/rollup-openbsd-x64": "4.61.1", "@rollup/rollup-openharmony-arm64": "4.61.1", "@rollup/rollup-win32-arm64-msvc": "4.61.1", "@rollup/rollup-win32-ia32-msvc": "4.61.1", "@rollup/rollup-win32-x64-gnu": "4.61.1", "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -259,6 +429,8 @@ "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -267,16 +439,34 @@ "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -301,6 +491,8 @@ "@routstr/sdk/@cashu/cashu-ts": ["@cashu/cashu-ts@3.7.0", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-Jy4Hek+Wouh94xS4InmTJRLpBPXA6fddVOAHR5lfNyTsCk9r3desY0vnwUGvOZBZWsSKZXedqdD/YpjWb3zngQ=="], + "@routstr/sdk/@types/node": ["@types/node@22.19.20", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw=="], + "@scure/bip39/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], "@scure/bip39/@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="], @@ -333,6 +525,10 @@ "nostr-tools/@scure/bip32": ["@scure/bip32@2.0.1", "", { "dependencies": { "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA=="], + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "@routstr/sdk/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "applesauce-common/applesauce-core/nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="], "applesauce-core/nostr-tools/@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="], diff --git a/package.json b/package.json index 5423e26..f6b26e6 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@cashu/cashu-ts": "^4.3.0", - "@routstr/sdk": "^0.3.9", + "@routstr/sdk": "file:../routstr-sdk", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 8cf95fd..6042314 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -1,11 +1,11 @@ import { createServer } from "http"; import { existsSync } from "fs"; import { - ModelManager, ProviderManager, createStorageAdapterFromStore, createSdkStore, } from "@routstr/sdk"; +import { ModelManager } from "@routstr/sdk/bun"; import type { SdkLogger } from "@routstr/sdk"; import { CONFIG_DIR, DB_PATH, SOCKET_PATH, PID_FILE } from "../utils/config"; import { logger } from "../utils/logger"; @@ -30,7 +30,7 @@ import { createBunSqliteUsageTrackingDriver, createShardedDiscoveryAdapter, createProviderRegistryFromDiscoveryAdapter, -} from "@routstr/sdk/storage"; +} from "@routstr/sdk/storage/bun"; import { createWalletAdapter } from "./wallet"; import type { AutoRefillConfig } from "./wallet/auto-refill"; import { createCocodClient } from "./wallet/cocod-client"; @@ -54,10 +54,8 @@ async function main(): Promise { const sqliteDriver = await createBunSqliteDriver(DB_PATH, { logger: daemonSdkLogger }); const { store, hydrate } = createSdkStore({ driver: sqliteDriver }); await hydrate; - const { Database } = await import("bun:sqlite"); - const usageTrackingDriver = createBunSqliteUsageTrackingDriver({ + const usageTrackingDriver = await createBunSqliteUsageTrackingDriver({ dbPath: DB_PATH, - sqlite: { Database }, legacyStorageDriver: sqliteDriver, }); From 70230d9030d123b9e64407af6a721456327c3b15 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sun, 7 Jun 2026 09:06:53 +0800 Subject: [PATCH 14/42] cached otkens now appear on the TUI --- src/daemon/types.ts | 6 ++++++ src/tui/usage/render.ts | 25 ++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/daemon/types.ts b/src/daemon/types.ts index 6744fb3..cf893b0 100644 --- a/src/daemon/types.ts +++ b/src/daemon/types.ts @@ -11,6 +11,12 @@ export type UsageData = { totalTokens: number; cost: number; satsCost: number; + provider?: string; + inputMsats?: number; + outputMsats?: number; + totalMsats?: number; + cacheReadInputTokens?: number; + cacheCreationInputTokens?: number; }; export type UsageTrackingEntry = UsageData & { diff --git a/src/tui/usage/render.ts b/src/tui/usage/render.ts index e22cfad..46e6f39 100644 --- a/src/tui/usage/render.ts +++ b/src/tui/usage/render.ts @@ -609,20 +609,35 @@ export function renderRecent(stats: UsageStats, width: number): string { if (recentEntries.length === 0) return renderBox(["No recent entries"], width, "Recent Requests"); const clientCol = 14; + const tokensCol = 18; + const costCol = 18; + const providerCol = Math.max(16, width - 4 - 10 - 18 - tokensCol - costCol - clientCol - 5); + const msatsToSats = (msats?: number) => typeof msats === "number" ? msats / 1000 : 0; const lines: string[] = []; - 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}`); + lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"MODEL".padEnd(18)} ${"I/CR/CW/O".padEnd(tokensCol)} ${"I/O/T in sats".padEnd(costCol)} ${"BASE:PROVIDER".padEnd(providerCol)} ${"CLIENT".slice(0, clientCol)}${COLORS.reset}`); lines.push(COLORS.dim + "─".repeat(width - 4) + COLORS.reset); for (const entry of recentEntries) { const time = formatTime(entry.timestamp).slice(0, 8); const model = entry.modelId.slice(0, 18).padEnd(18); - const tokens = `${formatNumber(entry.totalTokens).padEnd(6)} (${formatNumber(entry.promptTokens)}+${formatNumber(entry.completionTokens)})`; - const cost = `${formatCost(entry.satsCost).padEnd(8)} sats`; - const provider = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "").slice(0, 16).padEnd(16); + const tokens = [ + entry.promptTokens, + entry.cacheReadInputTokens || 0, + entry.cacheCreationInputTokens || 0, + entry.completionTokens, + ].map(formatNumber).join("/"); + const totalSats = typeof entry.totalMsats === "number" ? entry.totalMsats / 1000 : entry.satsCost; + const cost = [ + formatCost(msatsToSats(entry.inputMsats)), + formatCost(msatsToSats(entry.outputMsats)), + formatCost(totalSats), + ].join("/"); + const baseUrl = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", ""); + const provider = `${baseUrl}:${entry.provider || "unknown"}`.slice(0, providerCol).padEnd(providerCol); const clientName = (entry.client || "unknown").slice(0, clientCol - 1); const clientColor = CLIENT_COLORS[entry.client || "unknown"] || CLIENT_COLORS.default || COLORS.white; const modelColor = MODEL_COLORS[entry.modelId] || MODEL_COLORS.default; - 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}`); + lines.push(`${COLORS.dim}${time}${COLORS.reset} ${modelColor}${model}${COLORS.reset} ${tokens.padEnd(tokensCol)} ${COLORS.green}${cost.padEnd(costCol)}${COLORS.reset} ${COLORS.dim}${provider}${COLORS.reset} ${clientColor}${clientName}${COLORS.reset}`); } return renderBox(lines, width, `Recent Requests (${stats.entries.length} shown)`); From 27b13c30b0bde0c5e68be7debf2636ad06173381 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sun, 7 Jun 2026 10:56:06 +0800 Subject: [PATCH 15/42] made relays configurable --- src/daemon/index.ts | 2 ++ src/utils/config.ts | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 6042314..07d26ba 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -65,6 +65,8 @@ async function main(): Promise { const modelManager = new ModelManager(discoveryAdapter, { logger: daemonSdkLogger, eventStoreDbPath: `${CONFIG_DIR}/events.db`, + routstrPubkey: config.routstrPubkey, + nostrRelays: config.relays, }); // Create shared ProviderManager for consistent failure tracking across all requests const providerManager = new ProviderManager(providerRegistry, store, daemonSdkLogger); diff --git a/src/utils/config.ts b/src/utils/config.ts index a148ba5..0b19c75 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -41,6 +41,9 @@ export interface RoutstrdConfig { nsec?: string; /** Nostr hex pubkey for routstr review/model events (kind 38425/38423). */ routstrPubkey?: string; + /** Nostr relay URLs for provider/model discovery (kinds 38421/38423/38425). + * When unset, each method uses its own built-in defaults. */ + relays?: string[]; /** NWC integration configuration */ nwc?: NwcConfig; } From 4a66af87dafd4e3ca558ee232457e1927a57cfd3 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sun, 7 Jun 2026 11:13:39 +0800 Subject: [PATCH 16/42] feat(providers): add --refresh flag to providers list routstrd providers list --refresh now: - Re-fetches Nostr kind 38421 provider discovery events - Re-fetches Nostr kind 38423 routstr21 model list - Re-fetches Nostr kind 38425 review events (applies LGTM-based disable) - Fetches models from all discovered providers - Syncs fresh provider list and disabled status into the store Usage: routstrd providers list --refresh --- src/cli.ts | 10 ++++++-- src/daemon/http/index.ts | 10 ++++++++ src/daemon/index.ts | 3 ++- src/daemon/models.ts | 55 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 2da87d3..99f1514 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -743,10 +743,16 @@ const providersCmd = program providersCmd .command("list") .description("List all providers with their enabled/disabled status") - .action(async () => { + .option("--refresh", "Force re-fetch all Nostr events and refresh models from all enabled providers", false) + .action(async (options: { refresh: boolean }) => { await ensureDaemonRunning(); - const result = await callDaemon("/providers"); + const query = options.refresh ? "/providers?refresh=true" : "/providers"; + if (options.refresh) { + console.log("Force-refreshing providers from Nostr and fetching models...\n"); + } + + const result = await callDaemon(query); if (result.error) { console.log(result.error); process.exit(1); diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 1ed8a3e..5f5fbc9 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -41,6 +41,7 @@ type DaemonDeps = { ensureProvidersBootstrapped: () => Promise; getRoutstr21Models: (forceRefresh?: boolean) => Promise; getModelProviders: (modelId: string) => Promise; + refreshProvidersAndModels: () => Promise; mode?: ClientMode; /** Nostr hex pubkey for routstr review/model events (kind 38425/38423). */ routstrPubkey?: string; @@ -294,6 +295,7 @@ export function createDaemonRequestHandler(deps: { ensureProvidersBootstrapped: () => Promise; getRoutstr21Models: (forceRefresh?: boolean) => Promise; getModelProviders: (modelId: string) => Promise; + refreshProvidersAndModels: () => Promise; mode?: "xcashu" | "apikeys"; /** Nostr hex pubkey for routstr review/model events (kind 38425/38423). */ routstrPubkey?: string; @@ -1079,6 +1081,14 @@ export function createDaemonRequestHandler(deps: { if (req.method === "GET" && url.pathname === "/providers") { try { + const forceRefresh = + url.searchParams.get("refresh")?.toLowerCase() === "true"; + + if (forceRefresh) { + logger.log("Force-refreshing providers from Nostr and fetching models..."); + await deps.refreshProvidersAndModels(); + } + const state = deps.store.getState(); const baseUrlsList: string[] = state.baseUrlsList || []; const disabledProviders: string[] = state.disabledProviders || []; diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 07d26ba..c940a00 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -70,7 +70,7 @@ async function main(): Promise { }); // Create shared ProviderManager for consistent failure tracking across all requests const providerManager = new ProviderManager(providerRegistry, store, daemonSdkLogger); - const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders } = + const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, refreshProvidersAndModels } = createModelService(modelManager, store); const walletClient = createCocodClient({ cocodPath: config.cocodPath }); @@ -123,6 +123,7 @@ async function main(): Promise { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, + refreshProvidersAndModels, mode: config.mode || "apikeys", routstrPubkey: config.routstrPubkey, usageTrackingDriver, diff --git a/src/daemon/models.ts b/src/daemon/models.ts index abb0999..1f8a1ea 100644 --- a/src/daemon/models.ts +++ b/src/daemon/models.ts @@ -120,9 +120,64 @@ export function createModelService(modelManager: ModelManager, store: SdkStore) }; }; + /** + * Force-refresh everything: re-fetch Nostr provider discovery events, + * routstr21 model list, Nostr review events, and models from all enabled + * providers. Syncs the discovered provider list into the store. + */ + const refreshProvidersAndModels = async (): Promise => { + // Reset the bootstrap promise so we don't reuse cached results + providerBootstrapPromise = null; + + logger.log("Force-refreshing providers from Nostr..."); + + // Force-refresh provider discovery from Nostr (kind 38421) + const providers = await modelManager.bootstrapProviders(false, true); + logger.log(`Discovered ${providers.length} providers from Nostr`); + + // Force-refresh routstr21 models from Nostr (kind 38423) + const routstr21ModelIds = await modelManager.fetchRoutstr21Models(true); + logger.log(`Fetched ${routstr21ModelIds.length} routstr21 model IDs from Nostr`); + + // Force-refresh models from all providers + const models = await modelManager.fetchModels(providers, true); + logger.log(`Fetched ${models.length} models from ${providers.length} providers`); + + // Sync review events from Nostr (kind 38425) and apply disabled status + const reviewedDisabled = await modelManager.syncReviewedProvidersFromNostr( + providers, + undefined, + true, + ); + if (reviewedDisabled.length > 0) { + logger.log( + `Review sync disabled ${reviewedDisabled.length} provider(s): ${reviewedDisabled.join(", ")}`, + ); + } + + // Sync discovered providers into the store + const { baseUrlsList, setBaseUrlsList, disabledProviders, setDisabledProviders } = + store.getState() as any; + + // Replace baseUrlsList with the fresh provider list + setBaseUrlsList(providers); + + // Merge review-disabled providers into the store's disabled list + const existingDisabled = new Set(disabledProviders || []); + for (const url of reviewedDisabled) { + existingDisabled.add(url); + } + setDisabledProviders([...existingDisabled]); + + logger.log( + `Provider refresh complete: ${providers.length} total, ${existingDisabled.size} disabled`, + ); + }; + return { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, + refreshProvidersAndModels, }; } From d87a911241a8cc4ae2721041a04f964d98f55841 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sun, 7 Jun 2026 11:25:02 +0800 Subject: [PATCH 17/42] viisble lgos --- src/cli.ts | 4 ---- src/daemon/models.ts | 12 ++++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 99f1514..24f2e8c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -748,10 +748,6 @@ providersCmd await ensureDaemonRunning(); const query = options.refresh ? "/providers?refresh=true" : "/providers"; - if (options.refresh) { - console.log("Force-refreshing providers from Nostr and fetching models...\n"); - } - const result = await callDaemon(query); if (result.error) { console.log(result.error); diff --git a/src/daemon/models.ts b/src/daemon/models.ts index 1f8a1ea..702dd18 100644 --- a/src/daemon/models.ts +++ b/src/daemon/models.ts @@ -129,19 +129,19 @@ export function createModelService(modelManager: ModelManager, store: SdkStore) // Reset the bootstrap promise so we don't reuse cached results providerBootstrapPromise = null; - logger.log("Force-refreshing providers from Nostr..."); + console.log("Force-refreshing providers from Nostr..."); // Force-refresh provider discovery from Nostr (kind 38421) const providers = await modelManager.bootstrapProviders(false, true); - logger.log(`Discovered ${providers.length} providers from Nostr`); + console.log(`Discovered ${providers.length} providers from Nostr`); // Force-refresh routstr21 models from Nostr (kind 38423) const routstr21ModelIds = await modelManager.fetchRoutstr21Models(true); - logger.log(`Fetched ${routstr21ModelIds.length} routstr21 model IDs from Nostr`); + console.log(`Fetched ${routstr21ModelIds.length} routstr21 model IDs from Nostr`); // Force-refresh models from all providers const models = await modelManager.fetchModels(providers, true); - logger.log(`Fetched ${models.length} models from ${providers.length} providers`); + console.log(`Fetched ${models.length} models from ${providers.length} providers`); // Sync review events from Nostr (kind 38425) and apply disabled status const reviewedDisabled = await modelManager.syncReviewedProvidersFromNostr( @@ -150,7 +150,7 @@ export function createModelService(modelManager: ModelManager, store: SdkStore) true, ); if (reviewedDisabled.length > 0) { - logger.log( + console.log( `Review sync disabled ${reviewedDisabled.length} provider(s): ${reviewedDisabled.join(", ")}`, ); } @@ -169,7 +169,7 @@ export function createModelService(modelManager: ModelManager, store: SdkStore) } setDisabledProviders([...existingDisabled]); - logger.log( + console.log( `Provider refresh complete: ${providers.length} total, ${existingDisabled.size} disabled`, ); }; From 0ab608bf88d7dc0080d1a730c795d9ccff243e41 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:43:15 +0800 Subject: [PATCH 18/42] fixed provider disabling not working while running --- src/daemon/http/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 5f5fbc9..44ce3d7 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -817,6 +817,7 @@ export function createDaemonRequestHandler(deps: { } deps.store.getState().setDisabledProviders(disabledProviders); + deps.discoveryAdapter.setDisabledProviders(disabledProviders); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -873,6 +874,7 @@ export function createDaemonRequestHandler(deps: { } deps.store.getState().setDisabledProviders(disabledProviders); + deps.discoveryAdapter.setDisabledProviders(disabledProviders); res.writeHead(200, { "Content-Type": "application/json" }); res.end( From ae91c46f01b8f3f8adeb1b0e7c8d32c57cf5edd4 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:29:19 +0800 Subject: [PATCH 19/42] fix(usage-summary): compute size buckets in JS after SDK removed token-range filters The SDK dropped minTotalTokens/maxTotalTokens from AggregateUsageOptions (routstr-sdk c98de6b), so replace the five aggregate() calls with a single list() and bucket entries in-process. --- src/daemon/http/usage-summary.ts | 51 ++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/src/daemon/http/usage-summary.ts b/src/daemon/http/usage-summary.ts index a17460f..ddf7b5e 100644 --- a/src/daemon/http/usage-summary.ts +++ b/src/daemon/http/usage-summary.ts @@ -99,9 +99,36 @@ function emptyBucket(): SizeBucket { return { count: 0, cost: 0 }; } -function bucketFromRow(r: UsageAggregateRow | undefined): SizeBucket { - if (!r) return emptyBucket(); - return { count: r.requests, cost: r.satsCost }; +/** [minInclusive, maxExclusive) token bounds for each size bucket. */ +const SIZE_BUCKET_BOUNDS = { + tiny: [0, 1000], + small: [1000, 10000], + medium: [10000, 50000], + large: [50000, 100000], + huge: [100000, Infinity], +} as const; + +function computeSizeBuckets( + entries: UsageTrackingEntry[], +): UsageSummary["sizeBuckets"] { + const buckets = { + tiny: emptyBucket(), + small: emptyBucket(), + medium: emptyBucket(), + large: emptyBucket(), + huge: emptyBucket(), + }; + for (const entry of entries) { + for (const [name, [min, max]] of Object.entries(SIZE_BUCKET_BOUNDS)) { + if (entry.totalTokens >= min && entry.totalTokens < max) { + const bucket = buckets[name as keyof typeof buckets]; + bucket.count++; + bucket.cost += entry.satsCost; + break; + } + } + } + return buckets; } /** Returns the UTC ms for the start of the local day containing `now`. */ @@ -275,19 +302,11 @@ export async function getUsageSummary( })); // ── Size buckets ─────────────────────────────────────────────────────────── - const [tinyRow] = await driver.aggregate({ minTotalTokens: 0, maxTotalTokens: 1000 }); - const [smallRow] = await driver.aggregate({ minTotalTokens: 1000, maxTotalTokens: 10000 }); - const [mediumRow] = await driver.aggregate({ minTotalTokens: 10000, maxTotalTokens: 50000 }); - const [largeRow] = await driver.aggregate({ minTotalTokens: 50000, maxTotalTokens: 100000 }); - const [hugeRow] = await driver.aggregate({ minTotalTokens: 100000 }); - - const sizeBuckets = { - tiny: bucketFromRow(tinyRow), - small: bucketFromRow(smallRow), - medium: bucketFromRow(mediumRow), - large: bucketFromRow(largeRow), - huge: bucketFromRow(hugeRow), - }; + // The SDK's aggregate() no longer supports token-range filters + // (minTotalTokens/maxTotalTokens were removed in SDK pr-8), so bucket in + // JS from a single list() call instead of five aggregate() queries. + const allEntries = await driver.list(); + const sizeBuckets = computeSizeBuckets(allEntries); // ── Recent entries ───────────────────────────────────────────────────────── const recent = await driver.list({ limit: 50 }); From 829681ffca013fddb4980bf62de8fe499445ce3a Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 11 Jun 2026 08:33:15 +0800 Subject: [PATCH 20/42] removed legacy usage support --- src/tui/usage/app.ts | 4 +- src/tui/usage/data.ts | 36 ------------ src/tui/usage/render.ts | 123 ++++------------------------------------ src/tui/usage/types.ts | 2 +- 4 files changed, 13 insertions(+), 152 deletions(-) diff --git a/src/tui/usage/app.ts b/src/tui/usage/app.ts index 99faa77..8d95c96 100644 --- a/src/tui/usage/app.ts +++ b/src/tui/usage/app.ts @@ -1,6 +1,6 @@ import { getVisibleTabs } from "./constants.ts"; import type { Tab } from "./types.ts"; -import { fetchBalance, fetchClients, fetchStatus, fetchUsage, fetchUsageSummary, hasAnyNpubs, isDaemonRunning, type BalanceInfo, type ClientInfo, type StatusInfo } from "./data.ts"; +import { fetchBalance, fetchClients, fetchStatus, fetchUsageSummary, hasAnyNpubs, isDaemonRunning, type BalanceInfo, type ClientInfo, type StatusInfo } from "./data.ts"; import { applyScrollToContent, exitSearchMode, @@ -89,7 +89,7 @@ export async function runUsageTui(): Promise { const height = getHeight(); if (forceFetch || shouldUpdate) { - stats = await fetchUsageSummary() ?? await fetchUsage(10000); + stats = await fetchUsageSummary(); balance = await fetchBalance(); status = await fetchStatus(); clients = await fetchClients(); diff --git a/src/tui/usage/data.ts b/src/tui/usage/data.ts index b7dde44..b3e6681 100644 --- a/src/tui/usage/data.ts +++ b/src/tui/usage/data.ts @@ -76,42 +76,6 @@ export async function fetchBalance(): Promise { } } -export async function fetchUsage(limit = 10000): Promise { - try { - const running = await isDaemonRunning(); - if (!running) return null; - - const result = await callDaemon(`/usage?limit=${limit}`); - if (result.error) return null; - - // The auth proxy filters usage to the authenticated npub's clients and - // returns client IDs without the owner suffix. - const entries = result.output as UsageTrackingEntry[] | undefined; - const visibleEntries = Array.isArray(entries) ? entries : []; - - // Calculate totals from visible entries - const totals = visibleEntries.reduce( - (acc, entry) => ({ - promptTokens: acc.promptTokens + entry.promptTokens, - completionTokens: acc.completionTokens + entry.completionTokens, - totalTokens: acc.totalTokens + entry.totalTokens, - satsCost: acc.satsCost + entry.satsCost, - }), - { promptTokens: 0, completionTokens: 0, totalTokens: 0, satsCost: 0 }, - ); - - return { - entries: visibleEntries, - totalEntries: visibleEntries.length, - totalSatsCost: totals.satsCost, - recentSatsCost: totals.satsCost, // For now, recent = total since we don't have time window - limit, - }; - } catch { - return null; - } -} - export async function fetchUsageSummary(): Promise { try { const running = await isDaemonRunning(); diff --git a/src/tui/usage/render.ts b/src/tui/usage/render.ts index 6026e69..c5a6432 100644 --- a/src/tui/usage/render.ts +++ b/src/tui/usage/render.ts @@ -1,17 +1,8 @@ import { CLIENT_COLORS, COLORS, MODEL_COLORS } from "./constants.ts"; import type { Tab } from "./types.ts"; import { - formatDate, formatNumber, formatTime, - getClientStats, - getDayStats, - getHourlyToday, - getModelStats, - getNpubStats, - getProviderStats, - getTodayStart, - getTotals, type ClientInfo, } from "./data.ts"; import { vimState } from "./state.ts"; @@ -120,10 +111,7 @@ export function renderBarChart( export function renderOverview(stats: UsageStats, balance: BalanceInfo | null, status: StatusInfo | null, width: number): string { - // Token totals must come from the all-time summary (when present) to stay on - // the same horizon as totalRequests/totalSatsCost below — stats.entries holds - // only the latest 50 in summary mode. Fall back to entries for the legacy path. - const totals = stats.summary?.totals ?? getTotals(stats.entries); + const totals = stats.summary.totals; const totalRequests = stats.totalEntries; // Use server's total count, not entries.length const totalVisibleCost = stats.totalSatsCost; // <-- Use server's total, not client-side sum of limited entries const avgCost = totalRequests > 0 ? totalVisibleCost / totalRequests : 0; @@ -242,7 +230,7 @@ export function renderOverview(stats: UsageStats, balance: BalanceInfo | null, s output = renderBox(statusLines, width, "System Status") + "\n" + output; } - const modelStats = stats.summary?.models ?? getModelStats(stats.entries); + const modelStats = stats.summary.models; if (modelStats.length > 0) { const maxCost = modelStats[0]!.satsCost; const totalCost = Math.max(totalVisibleCost, 1); @@ -261,7 +249,7 @@ export function renderOverview(stats: UsageStats, balance: BalanceInfo | null, s output += "\n" + renderBox(modelLines, width, "Top Models by Cost"); } - const clientStats = stats.summary?.clients ?? getClientStats(stats.entries); + const clientStats = stats.summary.clients; if (clientStats.length > 0) { const maxCost = clientStats[0]!.satsCost; const totalCost = Math.max(totalVisibleCost, 1); @@ -308,23 +296,6 @@ export function renderToday(stats: UsageStats, width: number): string { // days[0] is only today when there was activity today. recentDays = days.filter((d) => d.date !== todayDateStr).slice(0, 6); hourlyMap = new Map(hoursToday.map((h) => [h.hour, { requests: h.requests, satsCost: h.satsCost }])); - } else { - // Fallback: compute from raw entries - const todayStart = getTodayStart(); - todayStats = { date: todayDateStr, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 }; - for (const entry of stats.entries) { - if (entry.timestamp >= todayStart) { - todayStats.requests++; - todayStats.satsCost += entry.satsCost; - todayStats.promptTokens += entry.promptTokens; - todayStats.completionTokens += entry.completionTokens; - todayStats.totalTokens += entry.totalTokens; - } - } - recentDays = Array.from(getDayStats(stats.entries).values()).slice(1, 7); - hourlyMap = new Map( - Array.from(getHourlyToday(stats.entries).entries()).map(([h, s]) => [h, { requests: s.requests, satsCost: s.satsCost }]) - ); } const summaryLines = [ @@ -375,7 +346,7 @@ export function renderToday(stats: UsageStats, width: number): string { } export function renderModels(stats: UsageStats, width: number): string { - const modelStats = stats.summary?.models ?? getModelStats(stats.entries); + const modelStats = stats.summary.models; if (modelStats.length === 0) return renderBox(["No model data available"], width, "Models"); // Use totalSatsCost (all-time) for percentage calculations to match header @@ -402,7 +373,7 @@ export function renderModels(stats: UsageStats, width: number): string { } export function renderProviders(stats: UsageStats, width: number): string { - const providerStats = stats.summary?.providers ?? getProviderStats(stats.entries); + const providerStats = stats.summary.providers; if (providerStats.length === 0) return renderBox(["No provider data available"], width, "Providers"); const lines: string[] = []; @@ -418,8 +389,8 @@ export function renderProviders(stats: UsageStats, width: number): string { } export function renderTokens(stats: UsageStats, width: number): string { - const totals = stats.summary?.totals ?? getTotals(stats.entries); - const modelStats = stats.summary?.models ?? getModelStats(stats.entries); + const totals = stats.summary.totals; + const modelStats = stats.summary.models; const summaryLines = [ `${COLORS.bold}Total Prompt Tokens:${COLORS.reset} ${formatNumber(totals.promptTokens)}`, `${COLORS.bold}Total Completion Tokens:${COLORS.reset} ${formatNumber(totals.completionTokens)}`, @@ -439,25 +410,7 @@ export function renderTokens(stats: UsageStats, width: number): string { output += "\n" + renderBox(tokenLines, width, "Tokens by Model"); } - const sizeBuckets = stats.summary?.sizeBuckets ?? (() => { - const b = { - tiny: { min: 0, max: 1000, count: 0, cost: 0 }, - small: { min: 1000, max: 10000, count: 0, cost: 0 }, - medium: { min: 10000, max: 50000, count: 0, cost: 0 }, - large: { min: 50000, max: 100000, count: 0, cost: 0 }, - huge: { min: 100000, max: Infinity, count: 0, cost: 0 }, - }; - for (const entry of stats.entries) { - for (const bucket of Object.values(b)) { - if (entry.totalTokens >= bucket.min && entry.totalTokens < bucket.max) { - bucket.count++; - bucket.cost += entry.satsCost; - break; - } - } - } - return b; - })(); + const sizeBuckets = stats.summary.sizeBuckets; const sizeLines = Object.entries(sizeBuckets).map(([name, bucket]) => `${name.padEnd(6)}: ${formatReqs(bucket.count).padStart(5)} reqs, ${formatCost(bucket.cost)} sats`); output += "\n" + renderBox(sizeLines, width, "Request Size Distribution"); @@ -465,7 +418,7 @@ export function renderTokens(stats: UsageStats, width: number): string { } export function renderClients(stats: UsageStats, width: number): string { - const clientStats = stats.summary?.clients ?? getClientStats(stats.entries); + const clientStats = stats.summary.clients; if (clientStats.length === 0) return renderBox(["No client data available (API key auth not used)"], width, "Client Breakdown"); // Use totalSatsCost (all-time) for percentage calculations to match header @@ -522,32 +475,6 @@ export function renderClients(stats: UsageStats, width: number): string { } clientModelLines.push(""); } - } else { - // Fallback: compute from raw entries; exclude the "unknown" bucket to match - // the summary path. - const clientModelMap = new Map>(); - for (const entry of stats.entries) { - const client = entry.client || "unknown"; - const model = entry.modelId; - if (!clientModelMap.has(client)) clientModelMap.set(client, new Map()); - const modelMap = clientModelMap.get(client)!; - const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 }; - modelMap.set(model, { - requests: existing.requests + 1, - satsCost: existing.satsCost + entry.satsCost, - tokens: existing.tokens + entry.totalTokens, - }); - } - for (const topClient of clientStats.filter((c) => c.client !== "unknown").slice(0, 3)) { - const modelMap = clientModelMap.get(topClient.client); - if (!modelMap) continue; - const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5); - clientModelLines.push(`${COLORS.bold}${topClient.client}${COLORS.reset} (${formatReqs(topClient.requests)} reqs, ${formatCost(topClient.satsCost)} sats)`); - for (const [model, data] of models) { - clientModelLines.push(` ${(MODEL_COLORS[model] || MODEL_COLORS.default)}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`); - } - clientModelLines.push(""); - } } if (clientModelLines.length > 0) { @@ -557,7 +484,7 @@ export function renderClients(stats: UsageStats, width: number): string { } export function renderNpubs(stats: UsageStats, clients: ClientInfo[], width: number): string { - const npubStats = stats.summary?.npubs ?? getNpubStats(stats.entries, clients); + const npubStats = stats.summary.npubs; if (npubStats.length === 0) return renderBox(["No npub data available"], width, "Npub Breakdown"); const totalCost = stats.totalSatsCost; @@ -614,36 +541,6 @@ export function renderNpubs(stats: UsageStats, clients: ClientInfo[], width: num } npubModelLines.push(""); } - } else { - // Fallback: compute from raw entries - const npubModelMap = new Map>(); - const clientNpubMap = new Map(); - for (const c of clients) { - if (c.ownerNpub) clientNpubMap.set(c.clientId, c.ownerNpub); - } - for (const entry of stats.entries) { - const npub = clientNpubMap.get(entry.client || ""); - if (!npub) continue; - const model = entry.modelId; - if (!npubModelMap.has(npub)) npubModelMap.set(npub, new Map()); - const modelMap = npubModelMap.get(npub)!; - const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 }; - modelMap.set(model, { - requests: existing.requests + 1, - satsCost: existing.satsCost + entry.satsCost, - tokens: existing.tokens + entry.totalTokens, - }); - } - for (const topNpub of npubStats.slice(0, 5)) { - const modelMap = npubModelMap.get(topNpub.npub); - if (!modelMap) continue; - const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5); - npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`); - for (const [model, data] of models) { - npubModelLines.push(` ${(MODEL_COLORS[model] || MODEL_COLORS.default)}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`); - } - npubModelLines.push(""); - } } if (npubModelLines.length > 0) { diff --git a/src/tui/usage/types.ts b/src/tui/usage/types.ts index d49476d..3db517f 100644 --- a/src/tui/usage/types.ts +++ b/src/tui/usage/types.ts @@ -10,7 +10,7 @@ export interface UsageStats { recentSatsCost: number; limit: number; /** Present when data was fetched via /usage/summary (server-aggregated path). */ - summary?: UsageSummary; + summary: UsageSummary; } export interface DayStats { From 764d2dd8269d55b020872e268535e7eef2dad942 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 11 Jun 2026 08:49:15 +0800 Subject: [PATCH 21/42] removed old functions with benchmark which is clearly not needed --- scripts/bench-usage.ts | 377 ---------------------------------------- src/tui/usage/data.ts | 160 +---------------- src/tui/usage/render.ts | 2 - src/tui/usage/types.ts | 46 ----- 4 files changed, 1 insertion(+), 584 deletions(-) delete mode 100644 scripts/bench-usage.ts diff --git a/scripts/bench-usage.ts b/scripts/bench-usage.ts deleted file mode 100644 index cc8a540..0000000 --- a/scripts/bench-usage.ts +++ /dev/null @@ -1,377 +0,0 @@ -/** - * bench-usage.ts — In-process, no-network benchmark (Option B). - * - * Compares the NEW server-aggregated summary path against the LEGACY - * "fetch-everything-then-re-aggregate" path, in terms of TIME, highlighting - * the daemon-side cache that lives in `getUsageSummary`. - * - * It deliberately SKIPS the HTTP layer. In production: - * - `fetchUsageSummary()` HTTP-calls the daemon, which runs `getUsageSummary()`. - * - `fetchUsage(N)` HTTP-calls the daemon, which runs `driver.list({limit:N})`. - * The real localhost socket round-trip (~1-3 ms) is roughly CONSTANT for both - * paths, so excluding it isolates the actual computational difference — which is - * the whole point. See the takeaway printed at the end. - * - * Run from the repo root: bun scripts/bench-usage.ts [--json] - * - */ - -// @ts-ignore — bun:sqlite is a Bun built-in module (matches storage/bun.ts). -import { Database } from "bun:sqlite"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { rmSync } from "node:fs"; - -import { createBunSqliteUsageTrackingDriver } from "@routstr/sdk/storage/bun"; -import { - getUsageSummary, - __resetUsageSummaryCacheForTest, -} from "../src/daemon/http/usage-summary.ts"; -import { - getTotals, - getModelStats, - getProviderStats, - getClientStats, - getDayStats, - getHourlyToday, - getNpubStats, -} from "../src/tui/usage/data.ts"; -import type { UsageTrackingEntry } from "../src/daemon/types.ts"; - -// ─── Config ────────────────────────────────────────────────────────────────── - -const SIZES = [100, 1000, 2500, 5000, 10000, 20000]; -const SAMPLES = 25; // median of 25, after a warmup -const STEADY_REFRESHES = 10; // the monitor's ~2s refresh loop -const TZ_OFFSET = 300; // synthetic UTC-5; fixed so results are reproducible -const DAY_SPREAD = 45; // spread timestamps over ~45 days - -const jsonMode = process.argv.includes("--json"); - -// ─── Synthetic, sanitized templates (no real DB, no real identifiers) ───────── - -interface Template { - modelId: string; - baseUrl: string; - client: string; - cost: number; - satsCost: number; - promptTokens: number; - completionTokens: number; -} - -const PROVIDER_A = "https://api.example-provider-a.com/"; -const PROVIDER_B = "https://api.example-provider-b.com/"; - -const TEMPLATES: Template[] = [ - { modelId: "minimax-m3", baseUrl: PROVIDER_A, client: "client-a", cost: 0.0021, satsCost: 4, promptTokens: 320, completionTokens: 180 }, - { modelId: "kimi-k2.6", baseUrl: PROVIDER_A, client: "client-b", cost: 0.0140, satsCost: 27, promptTokens: 5200, completionTokens: 2400 }, - { modelId: "deepseek-v4-flash", baseUrl: PROVIDER_B, client: "client-c", cost: 0.0008, satsCost: 1, promptTokens: 90, completionTokens: 60 }, - { modelId: "deepseek-v4-flash", baseUrl: PROVIDER_B, client: "client-a", cost: 0.0420, satsCost: 81, promptTokens: 24000, completionTokens: 13000 }, - { modelId: "kimi-k2.6", baseUrl: PROVIDER_B, client: "client-b", cost: 0.0065, satsCost: 12, promptTokens: 1800, completionTokens: 950 }, -]; - -// Clients array (shape works for both getUsageSummary's ClientEntry and -// data.ts's ClientInfo — both only read clientId / ownerNpub). ~half get a -// synthetic ownerNpub so the npub aggregation path is exercised. -const CLIENT_IDS = [...new Set(TEMPLATES.map((t) => t.client))]; -const clients = CLIENT_IDS.map((id, i) => ({ - clientId: id, - name: id, - apiKey: `apikey-${id}`, - createdAt: 0, - ownerNpub: i % 2 === 0 ? `npub1bench${id.replace(/[^a-z]/g, "")}` : undefined, -})); - -// ─── Row generation ──────────────────────────────────────────────────────────── - -function generateRows(n: number, now: number): UsageTrackingEntry[] { - const rows: UsageTrackingEntry[] = new Array(n); - const spreadMs = DAY_SPREAD * 86_400_000; - for (let i = 0; i < n; i++) { - const t = TEMPLATES[i % TEMPLATES.length]!; - // Spread timestamps backwards from `now` over DAY_SPREAD days, with a - // little jitter so multiple buckets/hours are populated. - const jitter = (i * 97) % 86_400_000; - const timestamp = now - Math.floor((i / n) * spreadMs) - jitter; - const promptTokens = t.promptTokens + (i % 17); - const completionTokens = t.completionTokens + (i % 13); - rows[i] = { - id: `bench-${i}`, - timestamp, - modelId: t.modelId, - baseUrl: t.baseUrl, - requestId: `req-${i}`, - cost: t.cost, - satsCost: t.satsCost, - promptTokens, - completionTokens, - totalTokens: promptTokens + completionTokens, - client: t.client, - sessionId: `sess-${i % 50}`, - tags: [], - }; - } - return rows; -} - -// ─── Seeding (raw transactional insert — fast) ──────────────────────────────── - -const TABLE = "usage_tracking"; - -function seedRawDb(dbPath: string, rows: UsageTrackingEntry[]): void { - const db = new Database(dbPath); - // Match the SDK schema exactly (copied from storage/bun.ts). - db.run(` - CREATE TABLE IF NOT EXISTS ${TABLE} ( - id TEXT PRIMARY KEY, - timestamp INTEGER NOT NULL, - model_id TEXT NOT NULL, - base_url TEXT NOT NULL, - request_id TEXT NOT NULL, - cost REAL NOT NULL, - sats_cost REAL NOT NULL, - prompt_tokens INTEGER NOT NULL, - completion_tokens INTEGER NOT NULL, - total_tokens INTEGER NOT NULL, - client TEXT, - session_id TEXT, - tags TEXT - ) - `); - db.run(`CREATE INDEX IF NOT EXISTS idx_${TABLE}_timestamp ON ${TABLE}(timestamp)`); - db.run(`CREATE INDEX IF NOT EXISTS idx_${TABLE}_model_id ON ${TABLE}(model_id)`); - db.run(`CREATE INDEX IF NOT EXISTS idx_${TABLE}_base_url ON ${TABLE}(base_url)`); - - const insert = db.query(` - INSERT OR REPLACE INTO ${TABLE} ( - id, timestamp, model_id, base_url, request_id, - cost, sats_cost, prompt_tokens, completion_tokens, total_tokens, - client, session_id, tags - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - const seed = db.transaction((batch: UsageTrackingEntry[]) => { - for (const r of batch) { - insert.run( - r.id, r.timestamp, r.modelId, r.baseUrl, r.requestId, - r.cost, r.satsCost, r.promptTokens, r.completionTokens, r.totalTokens, - r.client ?? null, r.sessionId ?? null, JSON.stringify(r.tags ?? []), - ); - } - }); - seed(rows); - db.close(); -} - -// ─── Timing helpers ──────────────────────────────────────────────────────────── - -async function timeMedian(fn: () => Promise, samples = SAMPLES): Promise { - await fn(); // warmup (discarded) - const times: number[] = []; - for (let i = 0; i < samples; i++) { - const start = performance.now(); - await fn(); - times.push(performance.now() - start); - } - times.sort((a, b) => a - b); - return times[Math.floor(times.length / 2)]!; -} - -function runLegacyAggregation(rows: UsageTrackingEntry[]): void { - // Mirror exactly what the monitor's render path does on raw entries. - getTotals(rows); - getModelStats(rows); - getProviderStats(rows); - getClientStats(rows); - getDayStats(rows); - getHourlyToday(rows); - getNpubStats(rows, clients); -} - -// ─── Bench per size ────────────────────────────────────────────────────────── - -interface SizeResult { - size: number; - summaryColdMs: number; - summaryWarmMs: number; - legacyMs: number; - summaryKB: number; - legacyKB: number; - reductionX: number; - steadySummaryMs: number; - steadyLegacyMs: number; - speedupX: number; -} - -async function benchSize(size: number): Promise { - const now = Date.now(); - const dbPath = join(tmpdir(), `routstrd-bench-${size}-${process.pid}-${Date.now()}.db`); - const rows = generateRows(size, now); - seedRawDb(dbPath, rows); - - const driver = await createBunSqliteUsageTrackingDriver({ dbPath }); - - try { - // ── summary cold: reset cache before every timed call ────────────────── - const summaryColdMs = await timeMedian(async () => { - __resetUsageSummaryCacheForTest(); - await getUsageSummary(driver, clients, TZ_OFFSET); - }); - - // ── summary warm: cache already populated (warmup call is itself a hit) ─ - __resetUsageSummaryCacheForTest(); - await getUsageSummary(driver, clients, TZ_OFFSET); // populate once - const summaryWarmMs = await timeMedian(async () => { - await getUsageSummary(driver, clients, TZ_OFFSET); - }); - - // ── legacy: list(limit:N) + full client-side re-aggregation, no cache ── - const legacyMs = await timeMedian(async () => { - const listed = await driver.list({ limit: size }); - runLegacyAggregation(listed); - }); - - // ── payload bytes ────────────────────────────────────────────────────── - __resetUsageSummaryCacheForTest(); - const summaryResult = await getUsageSummary(driver, clients, TZ_OFFSET); - const listedRows = await driver.list({ limit: size }); - const summaryBytes = JSON.stringify(summaryResult).length; - const legacyBytes = JSON.stringify(listedRows).length; - - // ── steady state (the 2s refresh loop, STEADY_REFRESHES iterations) ──── - // summary: 1 cold + (N-1) warm; legacy: N × full cost (no cache). - const steadySummaryMs = - summaryColdMs + (STEADY_REFRESHES - 1) * summaryWarmMs; - const steadyLegacyMs = STEADY_REFRESHES * legacyMs; - - return { - size, - summaryColdMs, - summaryWarmMs, - legacyMs, - summaryKB: summaryBytes / 1024, - legacyKB: legacyBytes / 1024, - reductionX: legacyBytes / summaryBytes, - steadySummaryMs, - steadyLegacyMs, - speedupX: steadyLegacyMs / steadySummaryMs, - }; - } finally { - // driver holds the only open handle now; closing the file by removing it. - try { - rmSync(dbPath, { force: true }); - rmSync(`${dbPath}-shm`, { force: true }); - rmSync(`${dbPath}-wal`, { force: true }); - } catch { /* ignore */ } - } -} - -// ─── Output ────────────────────────────────────────────────────────────────── - -function fmt(n: number, dp = 2): string { - return n.toFixed(dp); -} - -function pad(s: string, width: number): string { - return s.padStart(width); -} - -function printTable(results: SizeResult[]): void { - console.log(""); - console.log("Routstr usage path benchmark — TIME (in-process, no network)"); - console.log("Daemon-side summary cache vs. legacy fetch-all-and-re-aggregate.\n"); - - // ── Payload table (per request) ────────────────────────────── - console.log("Payload per request:"); - const payHeader = [ - pad("size", 7), - pad("summary", 11), - pad("legacy", 11), - pad("reduction", 11), - ].join(" "); - console.log(payHeader); - console.log("-".repeat(payHeader.length)); - for (const r of results) { - console.log([ - pad(String(r.size), 7), - pad(fmt(r.summaryKB, 1) + " KB", 11), - pad(fmt(r.legacyKB, 1) + " KB", 11), - pad(fmt(r.reductionX, 1) + "x", 11), - ].join(" ")); - } - - // ── Time table (ms) ────────────────────────────────────────── - console.log(""); - console.log("Time (ms):"); - const timeHeader = [ - pad("size", 7), - pad("sum cold", 10), - pad("sum warm", 10), - pad("legacy", 10), - pad("steady sum", 12), - pad("steady leg", 12), - pad("speedup", 9), - ].join(" "); - console.log(timeHeader); - console.log("-".repeat(timeHeader.length)); - for (const r of results) { - console.log([ - pad(String(r.size), 7), - pad(fmt(r.summaryColdMs) + "ms", 10), - pad(fmt(r.summaryWarmMs) + "ms", 10), - pad(fmt(r.legacyMs) + "ms", 10), - pad(fmt(r.steadySummaryMs) + "ms", 12), - pad(fmt(r.steadyLegacyMs) + "ms", 12), - pad(fmt(r.speedupX, 1) + "x", 9), - ].join(" ")); - } - - console.log(""); - console.log("Legend (in-process; times = medians of " + SAMPLES + " samples, warmup discarded;"); - console.log("excludes the ~constant localhost HTTP round-trip both paths would add):"); - console.log(" size seeded usage rows in the DB"); - console.log(""); - console.log(" Payload table:"); - console.log(" summary JSON bytes of the /usage/summary response (aggregates + 50 recent rows)"); - console.log(" legacy JSON bytes of the /usage response (all rows)"); - console.log(" reduction legacy / summary"); - console.log(""); - console.log(" Time table:"); - console.log(" sum cold summary build, COLD cache (cache miss: ~15-query rebuild + assemble)"); - console.log(" sum warm summary from WARM cache (count() + return the memoized result)"); - console.log(" legacy list all rows + the client-side re-aggregation the monitor does (no cache)"); - console.log(" steady sum total ms the SUMMARY path spends over " + STEADY_REFRESHES + " refreshes of the monitor's ~2s loop,"); - console.log(" assuming data is unchanged across the window (cache stays valid):"); - console.log(" 1 cold build + " + (STEADY_REFRESHES - 1) + " warm cache hits = cold + " + (STEADY_REFRESHES - 1) + "xwarm. Since warm is ~0.01ms,"); - console.log(" this is in practice ~one cold build."); - console.log(" steady leg same " + STEADY_REFRESHES + " refreshes for LEGACY = legacy x" + STEADY_REFRESHES + " (no cache, full cost each time)"); - console.log(" speedup steady leg / steady sum"); -} - -// ─── Main ──────────────────────────────────────────────────────────────────── - -async function main(): Promise { - const results: SizeResult[] = []; - for (const size of SIZES) { - results.push(await benchSize(size)); - } - - if (jsonMode) { - console.log(JSON.stringify( - { - config: { sizes: SIZES, samples: SAMPLES, steadyRefreshes: STEADY_REFRESHES, tzOffset: TZ_OFFSET }, - note: "In-process; HTTP round-trip (~1-3ms localhost, ~constant for both) excluded.", - results, - }, - null, - 2, - )); - } else { - printTable(results); - } -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/src/tui/usage/data.ts b/src/tui/usage/data.ts index b3e6681..66821af 100644 --- a/src/tui/usage/data.ts +++ b/src/tui/usage/data.ts @@ -1,6 +1,6 @@ import type { UsageTrackingEntry } from "../../daemon/types.ts"; import { callDaemon, isDaemonRunning } from "../../utils/daemon-client.ts"; -import type { ClientStats, DayStats, ModelStats, NpubStats, ProviderStats, UsageStats, UsageSummary } from "./types.ts"; +import type { UsageStats, UsageSummary } from "./types.ts"; export { isDaemonRunning }; @@ -101,17 +101,6 @@ export async function fetchUsageSummary(): Promise { } } -export function getTodayStart(): number { - const now = new Date(); - return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); -} - -export function formatDate(timestamp: number): string { - const d = new Date(timestamp); - const p = (n: number) => String(n).padStart(2, "0"); - return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; -} - export function formatTime(timestamp: number): string { const d = new Date(timestamp); const p = (n: number) => String(n).padStart(2, "0"); @@ -124,100 +113,6 @@ export function formatNumber(n: number): string { return n.toString(); } -export function getDayStats(entries: UsageTrackingEntry[]): Map { - const days = new Map(); - for (const entry of entries) { - const date = formatDate(entry.timestamp); - const existing = days.get(date) || { date, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 }; - days.set(date, { - ...existing, - requests: existing.requests + 1, - satsCost: existing.satsCost + entry.satsCost, - promptTokens: existing.promptTokens + entry.promptTokens, - completionTokens: existing.completionTokens + entry.completionTokens, - totalTokens: existing.totalTokens + entry.totalTokens, - }); - } - return days; -} - -export function getHourlyToday(entries: UsageTrackingEntry[]): Map { - const todayStart = getTodayStart(); - const hours = new Map(); - for (const entry of entries) { - if (entry.timestamp < todayStart) continue; - const hour = new Date(entry.timestamp).getHours(); - const existing = hours.get(hour) || { - date: formatDate(entry.timestamp), requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, - }; - hours.set(hour, { - ...existing, - requests: existing.requests + 1, - satsCost: existing.satsCost + entry.satsCost, - promptTokens: existing.promptTokens + entry.promptTokens, - completionTokens: existing.completionTokens + entry.completionTokens, - totalTokens: existing.totalTokens + entry.totalTokens, - }); - } - return hours; -} - -export function getModelStats(entries: UsageTrackingEntry[]): ModelStats[] { - const models = new Map(); - for (const entry of entries) { - const existing = models.get(entry.modelId) || { - modelId: entry.modelId, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, - }; - models.set(entry.modelId, { - ...existing, - requests: existing.requests + 1, - satsCost: existing.satsCost + entry.satsCost, - promptTokens: existing.promptTokens + entry.promptTokens, - completionTokens: existing.completionTokens + entry.completionTokens, - totalTokens: existing.totalTokens + entry.totalTokens, - }); - } - return Array.from(models.values()).sort((a, b) => b.satsCost - a.satsCost); -} - -export function getProviderStats(entries: UsageTrackingEntry[]): ProviderStats[] { - const providers = new Map(); - for (const entry of entries) { - const url = entry.baseUrl || "unknown"; - const existing = providers.get(url) || { - baseUrl: url, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, - }; - providers.set(url, { - ...existing, - requests: existing.requests + 1, - satsCost: existing.satsCost + entry.satsCost, - promptTokens: existing.promptTokens + entry.promptTokens, - completionTokens: existing.completionTokens + entry.completionTokens, - totalTokens: existing.totalTokens + entry.totalTokens, - }); - } - return Array.from(providers.values()).sort((a, b) => b.satsCost - a.satsCost); -} - -export function getClientStats(entries: UsageTrackingEntry[]): ClientStats[] { - const clients = new Map(); - for (const entry of entries) { - const client = entry.client || "unknown"; - const existing = clients.get(client) || { - client, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, - }; - clients.set(client, { - ...existing, - requests: existing.requests + 1, - satsCost: existing.satsCost + entry.satsCost, - promptTokens: existing.promptTokens + entry.promptTokens, - completionTokens: existing.completionTokens + entry.completionTokens, - totalTokens: existing.totalTokens + entry.totalTokens, - }); - } - return Array.from(clients.values()).sort((a, b) => b.satsCost - a.satsCost); -} - // ─── Client / Npub helpers ──────────────────────────────────────────── export interface ClientInfo { @@ -251,56 +146,3 @@ export async function fetchClients(): Promise { export function hasAnyNpubs(clients: ClientInfo[]): boolean { return clients.some((c) => !!c.ownerNpub); } - -export function getNpubStats(entries: UsageTrackingEntry[], clients: ClientInfo[]): NpubStats[] { - // Build client-id → ownerNpub lookup - const clientNpubMap = new Map(); - for (const c of clients) { - if (c.ownerNpub) { - clientNpubMap.set(c.clientId, c.ownerNpub); - } - } - - // Aggregate usage per npub - const npubs = new Map(); - for (const entry of entries) { - const npub = clientNpubMap.get(entry.client || ""); - if (!npub) continue; // skip entries whose client has no ownerNpub - - const existing = npubs.get(npub) || { - npub, - requests: 0, - satsCost: 0, - promptTokens: 0, - completionTokens: 0, - totalTokens: 0, - }; - npubs.set(npub, { - ...existing, - requests: existing.requests + 1, - satsCost: existing.satsCost + entry.satsCost, - promptTokens: existing.promptTokens + entry.promptTokens, - completionTokens: existing.completionTokens + entry.completionTokens, - totalTokens: existing.totalTokens + entry.totalTokens, - }); - } - - return Array.from(npubs.values()).sort((a, b) => b.satsCost - a.satsCost); -} - -// ─── Totals ─────────────────────────────────────────────────────────── - -export function getTotals(entries: UsageTrackingEntry[] | undefined) { - if (!entries || !Array.isArray(entries)) { - return { promptTokens: 0, completionTokens: 0, totalTokens: 0, satsCost: 0 }; - } - return entries.reduce( - (acc, entry) => ({ - promptTokens: acc.promptTokens + entry.promptTokens, - completionTokens: acc.completionTokens + entry.completionTokens, - totalTokens: acc.totalTokens + entry.totalTokens, - satsCost: acc.satsCost + entry.satsCost, - }), - { promptTokens: 0, completionTokens: 0, totalTokens: 0, satsCost: 0 } - ); -} diff --git a/src/tui/usage/render.ts b/src/tui/usage/render.ts index c5a6432..2fe7b41 100644 --- a/src/tui/usage/render.ts +++ b/src/tui/usage/render.ts @@ -275,8 +275,6 @@ export function renderToday(stats: UsageStats, width: number): string { const currentHour = new Date().getHours(); // Build today's date string from LOCAL date components so it matches the // server's tz-bucketed day keys (which use the client's tzOffsetMinutes). - // formatDate() returns a UTC date via toISOString() and would mismatch for - // non-UTC users. const _now = new Date(); const todayDateStr = `${_now.getFullYear()}-${String(_now.getMonth() + 1).padStart(2, "0")}-${String(_now.getDate()).padStart(2, "0")}`; diff --git a/src/tui/usage/types.ts b/src/tui/usage/types.ts index 3db517f..2c0345d 100644 --- a/src/tui/usage/types.ts +++ b/src/tui/usage/types.ts @@ -9,46 +9,9 @@ export interface UsageStats { totalSatsCost: number; recentSatsCost: number; limit: number; - /** Present when data was fetched via /usage/summary (server-aggregated path). */ summary: UsageSummary; } -export interface DayStats { - date: string; - requests: number; - satsCost: number; - promptTokens: number; - completionTokens: number; - totalTokens: number; -} - -export interface ModelStats { - modelId: string; - requests: number; - satsCost: number; - promptTokens: number; - completionTokens: number; - totalTokens: number; -} - -export interface ProviderStats { - baseUrl: string; - requests: number; - satsCost: number; - promptTokens: number; - completionTokens: number; - totalTokens: number; -} - -export interface ClientStats { - client: string; - requests: number; - satsCost: number; - promptTokens: number; - completionTokens: number; - totalTokens: number; -} - export type TabId = "overview" | "today" | "models" | "providers" | "tokens" | "clients" | "npubs" | "recent"; export interface Tab { @@ -57,15 +20,6 @@ export interface Tab { key: string; } -export interface NpubStats { - npub: string; - requests: number; - satsCost: number; - promptTokens: number; - completionTokens: number; - totalTokens: number; -} - export interface VimState { scrollPos: number; searchQuery: string; From 61acc0b5fb996ce5ced2b7ff2a1d7216f9971404 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:17:13 +0800 Subject: [PATCH 22/42] added npub based filter! --- src/daemon/http/index.ts | 8 ++++- src/daemon/http/usage-summary.ts | 54 +++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 7132315..89d453c 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -1139,8 +1139,14 @@ export function createDaemonRequestHandler(deps: { if (req.method === "GET" && url.pathname === "/usage/summary") { try { const tz = Number.parseInt(url.searchParams.get("tz") || "0", 10) || 0; + const npubFilter = url.searchParams.get("npub")?.trim(); const clients = getClientsFromStore(deps.store); - const summary = await getUsageSummary(deps.usageTrackingDriver, clients, tz); + const clientFilter = npubFilter + ? clients + .filter((c) => c.ownerNpub === npubFilter) + .map((c) => c.clientId) + : undefined; + const summary = await getUsageSummary(deps.usageTrackingDriver, clients, tz, clientFilter); sendJson(res, 200, { output: summary }); } catch (error) { sendJson(res, 500, { error: toErrorMessage(error) }); diff --git a/src/daemon/http/usage-summary.ts b/src/daemon/http/usage-summary.ts index ddf7b5e..e909fa2 100644 --- a/src/daemon/http/usage-summary.ts +++ b/src/daemon/http/usage-summary.ts @@ -160,13 +160,14 @@ export async function getUsageSummary( driver: UsageTrackingDriver, clients: ClientEntry[], tzOffsetMinutes: number, + /** If set, only include usage for these client IDs (e.g. from `?npub=` filtering). */ + clientFilter?: string[], ): Promise { - // Cache key: total row count + per-client identity (id + ownerNpub) + tz. - // Including ownerNpub ensures a client assignment change invalidates the cache - // even though the row count doesn't change. - const count = await driver.count(); + // Cache key: total row count + per-client identity + filter + tz. + const count = await driver.count(clientFilter ? { clients: clientFilter } : {}); const clientIdentity = clients.map((c) => `${c.clientId}:${c.ownerNpub ?? ""}`).join(","); - const cacheKey = `${count}:${clientIdentity}:${tzOffsetMinutes}`; + const filterKey = clientFilter ? `:f:${clientFilter.sort().join(",")}` : ""; + const cacheKey = `${count}:${clientIdentity}:${tzOffsetMinutes}${filterKey}`; const now = Date.now(); if ( @@ -177,29 +178,57 @@ export async function getUsageSummary( return _cache.summary; } + // Base filter applied to every aggregate/list/count call + const baseFilter = clientFilter ? { clients: clientFilter } as const : {}; + + // Short-circuit: if the filter yields no rows, return zeroed summary + if (count === 0) { + const zeroStat: StatRow = { + requests: 0, promptTokens: 0, completionTokens: 0, + totalTokens: 0, cost: 0, satsCost: 0, + }; + const zeroSummary: UsageSummary = { + generatedAt: now, + totals: zeroStat, + models: [], + providers: [], + clients: [], + npubs: [], + days: [], + hoursToday: [], + sizeBuckets: { + tiny: emptyBucket(), small: emptyBucket(), medium: emptyBucket(), + large: emptyBucket(), huge: emptyBucket(), + }, + recent: [], + }; + _cache = { key: cacheKey, summary: zeroSummary }; + return zeroSummary; + } + // ── Totals ───────────────────────────────────────────────────────────────── - const [totalsRow] = await driver.aggregate({}); + const [totalsRow] = await driver.aggregate({ ...baseFilter }); const totals: StatRow = totalsRow ? rowToStat(totalsRow) : { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: 0, satsCost: 0, }; // ── Models ───────────────────────────────────────────────────────────────── - const modelRows = await driver.aggregate({ groupBy: "modelId" }); + const modelRows = await driver.aggregate({ ...baseFilter, groupBy: "modelId" }); const models: ModelSummary[] = modelRows.map((r) => ({ modelId: r.group ?? "unknown", ...rowToStat(r), })); // ── Providers ────────────────────────────────────────────────────────────── - const providerRows = await driver.aggregate({ groupBy: "baseUrl" }); + const providerRows = await driver.aggregate({ ...baseFilter, groupBy: "baseUrl" }); const providers: ProviderSummary[] = providerRows.map((r) => ({ baseUrl: r.group ?? "unknown", ...rowToStat(r), })); // ── Clients ──────────────────────────────────────────────────────────────── - const clientRows = await driver.aggregate({ groupBy: "client" }); + const clientRows = await driver.aggregate({ ...baseFilter, groupBy: "client" }); const clientSummaries: ClientSummary[] = clientRows.map((r) => ({ client: r.group ?? "unknown", ...rowToStat(r), @@ -213,6 +242,7 @@ export async function getUsageSummary( for (let i = 0; i < topClientRows.length; i++) { const clientId = topClientRows[i]!.group!; const topModelRows = await driver.aggregate({ + ...baseFilter, groupBy: "modelId", client: clientId, }); @@ -281,6 +311,7 @@ export async function getUsageSummary( // ── Days (last 30, most-recent-first) ───────────────────────────────────── const dayRows = await driver.aggregate({ + ...baseFilter, groupBy: "day", tzOffsetMinutes, after: now - 30 * 86400000, @@ -292,6 +323,7 @@ export async function getUsageSummary( // ── Hours today ──────────────────────────────────────────────────────────── const todayStartUtc = startOfLocalDayUtc(now, tzOffsetMinutes); const hourRows = await driver.aggregate({ + ...baseFilter, groupBy: "hour", tzOffsetMinutes, after: todayStartUtc - 1, @@ -305,11 +337,11 @@ export async function getUsageSummary( // The SDK's aggregate() no longer supports token-range filters // (minTotalTokens/maxTotalTokens were removed in SDK pr-8), so bucket in // JS from a single list() call instead of five aggregate() queries. - const allEntries = await driver.list(); + const allEntries = await driver.list(baseFilter); const sizeBuckets = computeSizeBuckets(allEntries); // ── Recent entries ───────────────────────────────────────────────────────── - const recent = await driver.list({ limit: 50 }); + const recent = await driver.list({ ...baseFilter, limit: 50 }); const summary: UsageSummary = { generatedAt: now, From 8c52c90f94477a21f7396bd7eda354696e16b229 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:31:32 +0800 Subject: [PATCH 23/42] addeed npub filtering for /usage as well --- src/daemon/http/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 89d453c..28f3974 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -1125,8 +1125,16 @@ export function createDaemonRequestHandler(deps: { if (req.method === "GET" && url.pathname === "/usage") { try { + const npubFilter = url.searchParams.get("npub")?.trim(); + const clients = npubFilter ? getClientsFromStore(deps.store) : undefined; + const clientFilter = npubFilter + ? clients! + .filter((c) => c.ownerNpub === npubFilter) + .map((c) => c.clientId) + : undefined; const output = await deps.usageTrackingDriver.list({ limit: parseLimit(url.searchParams.get("limit")), + ...(clientFilter ? { clients: clientFilter } : {}), }); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ output })); From 1735d97a9e00e523202f073294f59cf60d46b9e7 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sat, 20 Jun 2026 10:10:21 +0800 Subject: [PATCH 24/42] feat: configure raw request response logging --- src/daemon/http/index.ts | 5 +++++ src/daemon/index.ts | 17 ++++++++++++++++- src/utils/config.ts | 8 ++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 28f3974..2cfc503 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -48,6 +48,7 @@ type DaemonDeps = { routstrPubkey?: string; providerManager: ProviderManager; refundClient: any; + requestResponseLogDir?: string; }; /** @@ -303,6 +304,7 @@ export function createDaemonRequestHandler(deps: { usageTrackingDriver: UsageTrackingDriver; providerManager: ProviderManager; refundClient: any; + requestResponseLogDir?: string; }) { return async function handler(req: IncomingMessage, res: ServerResponse) { const host = req.headers.host || "localhost"; @@ -1274,6 +1276,9 @@ export function createDaemonRequestHandler(deps: { sdkStore: deps.store, providerManager: deps.providerManager, logger: reqLogger, + ...(deps.requestResponseLogDir + ? { requestResponseLogDir: deps.requestResponseLogDir } + : {}), ...(deps.routstrPubkey ? { routstrPubkey: deps.routstrPubkey } : {}), }); diff --git a/src/daemon/index.ts b/src/daemon/index.ts index bf26043..f7538a7 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -11,7 +11,13 @@ import { // (SDK 0.3.7+ browser-safe entrypoint split). import { ModelManager } from "@routstr/sdk/bun"; import type { SdkLogger } from "@routstr/sdk"; -import { CONFIG_DIR, DB_PATH, SOCKET_PATH, PID_FILE } from "../utils/config"; +import { + CONFIG_DIR, + DB_PATH, + SOCKET_PATH, + PID_FILE, + REQUEST_RESPONSE_LOGS_DIR, +} from "../utils/config"; import { logger } from "../utils/logger"; @@ -49,6 +55,11 @@ async function main(): Promise { const port = args.port; const provider = args.provider || config.provider; + const requestResponseLogDir = + process.env.ROUTSTRD_REQUEST_RESPONSE_LOG_DIR || + (config.requestResponseLogging?.enabled + ? config.requestResponseLogging.dir || REQUEST_RESPONSE_LOGS_DIR + : undefined); await ensureDirs(); @@ -133,6 +144,7 @@ async function main(): Promise { usageTrackingDriver, providerManager, refundClient, + requestResponseLogDir, }), ); @@ -242,6 +254,9 @@ async function main(): Promise { server.listen(port, async () => { logger.log(`Routstr daemon listening on http://localhost:${port}/v1`); + if (requestResponseLogDir) { + logger.log(`Raw request/response logs: ${requestResponseLogDir}`); + } // Start the recurring model refresh job after initial bootstrap void ensureProvidersBootstrapped() diff --git a/src/utils/config.ts b/src/utils/config.ts index 0b19c75..202b2ca 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -6,6 +6,7 @@ export const PID_FILE = process.env.ROUTSTRD_PID || `${CONFIG_DIR}/routstrd.pid` export const DB_PATH = `${CONFIG_DIR}/routstr.db`; export const CONFIG_FILE = `${CONFIG_DIR}/config.json`; export const LOGS_DIR = `${CONFIG_DIR}/logs`; +export const REQUEST_RESPONSE_LOGS_DIR = `${CONFIG_DIR}/request-response-logs`; /** NWC auto-refill configuration */ export interface NwcAutoRefillConfig { @@ -34,6 +35,13 @@ export interface RoutstrdConfig { provider: string | null; cocodPath: string | null; mode?: "xcashu" | "apikeys"; + /** Raw upstream request/response logging. Disabled by default because logs can contain sensitive prompts, outputs, and auth/payment headers. */ + requestResponseLogging?: { + /** Enable raw request/response file logging. */ + enabled?: boolean; + /** Root log directory. SDK writes requests/*.json and responses/*.jsonl below this directory. Defaults to ~/.routstrd/request-response-logs. */ + dir?: string; + }; daemonUrl?: string; /** URL of the auth proxy (routstrd-auth) for management endpoints (npubs, clients, usage). * Defaults to daemonUrl or localhost:{port} if not set. */ From f15e18cf05d6ec3b47920cf46d59d3e2e3de132f Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:00:52 +0800 Subject: [PATCH 25/42] Adapt request response logging to SDK sink --- src/daemon/http/index.ts | 9 +- src/daemon/index.ts | 9 +- src/daemon/request-response-log-sink.ts | 269 ++++++++++++++++++++++++ 3 files changed, 282 insertions(+), 5 deletions(-) create mode 100644 src/daemon/request-response-log-sink.ts diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 2cfc503..b66c3fa 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -7,6 +7,7 @@ import { ProviderManager, } from "@routstr/sdk"; import type { UsageTrackingDriver, SdkLogger } from "@routstr/sdk"; +import type { RequestResponseLogSink } from "../request-response-log-sink"; import { logger } from "../../utils/logger"; import { loadDaemonConfig, saveDaemonConfig } from "../config-store"; import { @@ -48,7 +49,7 @@ type DaemonDeps = { routstrPubkey?: string; providerManager: ProviderManager; refundClient: any; - requestResponseLogDir?: string; + requestResponseLogSink?: RequestResponseLogSink; }; /** @@ -304,7 +305,7 @@ export function createDaemonRequestHandler(deps: { usageTrackingDriver: UsageTrackingDriver; providerManager: ProviderManager; refundClient: any; - requestResponseLogDir?: string; + requestResponseLogSink?: RequestResponseLogSink; }) { return async function handler(req: IncomingMessage, res: ServerResponse) { const host = req.headers.host || "localhost"; @@ -1276,8 +1277,8 @@ export function createDaemonRequestHandler(deps: { sdkStore: deps.store, providerManager: deps.providerManager, logger: reqLogger, - ...(deps.requestResponseLogDir - ? { requestResponseLogDir: deps.requestResponseLogDir } + ...(deps.requestResponseLogSink + ? { requestResponseLogSink: deps.requestResponseLogSink } : {}), ...(deps.routstrPubkey ? { routstrPubkey: deps.routstrPubkey } : {}), }); diff --git a/src/daemon/index.ts b/src/daemon/index.ts index f7538a7..9875400 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -46,6 +46,7 @@ import type { AutoRefillConfig } from "./wallet/auto-refill"; import { createCocodClient } from "./wallet/cocod-client"; import { createModelService } from "./models"; import { createDaemonRequestHandler } from "./http"; +import { FileRequestResponseLogSink } from "./request-response-log-sink"; import { refreshModelsAndIntegrations } from "../integrations"; import { RoutstrClient } from "@routstr/sdk"; @@ -60,6 +61,12 @@ async function main(): Promise { (config.requestResponseLogging?.enabled ? config.requestResponseLogging.dir || REQUEST_RESPONSE_LOGS_DIR : undefined); + const requestResponseLogSink = requestResponseLogDir + ? new FileRequestResponseLogSink({ + dir: requestResponseLogDir, + logger: daemonSdkLogger.child("request-response-log"), + }) + : undefined; await ensureDirs(); @@ -144,7 +151,7 @@ async function main(): Promise { usageTrackingDriver, providerManager, refundClient, - requestResponseLogDir, + requestResponseLogSink, }), ); diff --git a/src/daemon/request-response-log-sink.ts b/src/daemon/request-response-log-sink.ts new file mode 100644 index 0000000..19350b3 --- /dev/null +++ b/src/daemon/request-response-log-sink.ts @@ -0,0 +1,269 @@ +import { createWriteStream, mkdirSync, type WriteStream } from "fs"; +import { writeFile } from "fs/promises"; +import { join } from "path"; +import type { SdkLogger } from "@routstr/sdk"; + +export interface RequestResponseLogRequestInput { + method: string; + url: string; + path: string; + baseUrl: string; + headers: Record; + body?: unknown; + rawBody?: string; +} + +export interface RequestResponseLogSink { + logRequest?(input: RequestResponseLogRequestInput): string | undefined | Promise; + logResponseStart?(id: string | undefined, response: Response): void | Promise; + logResponseChunk?(id: string | undefined, sequence: number, text: string): void | Promise; + logResponseEnd?(id: string | undefined): void | Promise; + logResponseError?(id: string | undefined, error: unknown): void | Promise; + logResponseBody?(id: string | undefined, response: Response): void | Promise; +} + +interface ActiveResponseLog { + stream: WriteStream; + pending: Promise; +} + +export interface FileRequestResponseLogSinkOptions { + dir: string; + logger?: SdkLogger; +} + +const SENSITIVE_HEADER_NAMES = new Set([ + "authorization", + "x-cashu", + "cookie", + "set-cookie", + "proxy-authorization", +]); + +const SENSITIVE_BODY_FIELD_NAMES = new Set([ + "authorization", + "api_key", + "apikey", + "apiKey", + "access_token", + "accessToken", + "bearer", + "cashu", + "cookie", + "key", + "password", + "secret", + "token", + "x-cashu", +]); + +const REDACTED = "[REDACTED]"; + +const sanitizeForFilename = (value: string): string => + value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, ""); + +const makeId = (): string => { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const random = crypto.randomUUID().slice(0, 8); + return `${timestamp}-${random}`; +}; + +const headersToObject = (headers: Headers): Record => { + const out: Record = {}; + headers.forEach((value, key) => { + out[key] = value; + }); + return out; +}; + +const redactHeaders = (headers: Record): Record => + Object.fromEntries( + Object.entries(headers).map(([key, value]) => [ + key, + SENSITIVE_HEADER_NAMES.has(key.toLowerCase()) ? REDACTED : value, + ]), + ); + +const redactBody = (value: unknown): unknown => { + if (!value || typeof value !== "object") return value; + if (Array.isArray(value)) return value.map(redactBody); + + return Object.fromEntries( + Object.entries(value as Record).map(([key, entry]) => [ + key, + SENSITIVE_BODY_FIELD_NAMES.has(key) || SENSITIVE_BODY_FIELD_NAMES.has(key.toLowerCase()) + ? REDACTED + : redactBody(entry), + ]), + ); +}; + +const redactRawBody = (rawBody: string | undefined): string | undefined => { + if (!rawBody) return undefined; + try { + return JSON.stringify(redactBody(JSON.parse(rawBody))); + } catch { + return rawBody; + } +}; + +export class FileRequestResponseLogSink implements RequestResponseLogSink { + private requestsDir: string; + private responsesDir: string; + private activeResponses = new Map(); + + constructor(private options: FileRequestResponseLogSinkOptions) { + this.requestsDir = join(options.dir, "requests"); + this.responsesDir = join(options.dir, "responses"); + mkdirSync(this.requestsDir, { recursive: true }); + mkdirSync(this.responsesDir, { recursive: true }); + } + + async logRequest(input: RequestResponseLogRequestInput): Promise { + try { + const id = makeId(); + const filePath = join(this.requestsDir, `${sanitizeForFilename(id)}.json`); + await writeFile( + filePath, + JSON.stringify( + { + id, + timestamp: new Date().toISOString(), + method: input.method, + url: input.url, + path: input.path, + baseUrl: input.baseUrl, + headers: redactHeaders(input.headers), + body: redactBody(input.body), + rawBody: redactRawBody(input.rawBody), + }, + null, + 2, + ), + ); + return id; + } catch (error) { + this.options.logger?.error?.("[request-response-log] failed to log request:", error); + return undefined; + } + } + + async logResponseStart(id: string | undefined, response: Response): Promise { + if (!id) return; + await this.append(id, { + type: "response_start", + status: response.status, + statusText: response.statusText, + headers: redactHeaders(headersToObject(response.headers)), + }); + } + + logResponseChunk(id: string | undefined, sequence: number, text: string): void { + if (!id) return; + void this.append(id, { + type: "chunk", + sequence, + text, + }); + } + + async logResponseEnd(id: string | undefined): Promise { + if (!id) return; + await this.append(id, { type: "end" }); + await this.close(id); + } + + async logResponseError(id: string | undefined, error: unknown): Promise { + if (!id) return; + await this.append(id, { + type: "error", + error: error instanceof Error ? { message: error.message, stack: error.stack } : String(error), + }); + await this.close(id); + } + + async logResponseBody(id: string | undefined, response: Response): Promise { + if (!id) return; + try { + if (!response.body) { + await this.logResponseEnd(id); + return; + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let sequence = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (value && value.byteLength > 0) { + await this.append(id, { + type: "chunk", + sequence: sequence++, + text: decoder.decode(value, { stream: true }), + }); + } + } + + const tail = decoder.decode(); + if (tail) { + await this.append(id, { + type: "chunk", + sequence: sequence++, + text: tail, + }); + } + + await this.logResponseEnd(id); + } catch (error) { + await this.logResponseError(id, error); + } + } + + private getOrCreate(id: string): ActiveResponseLog { + const existing = this.activeResponses.get(id); + if (existing) return existing; + + const filePath = join(this.responsesDir, `${sanitizeForFilename(id)}.jsonl`); + const stream = createWriteStream(filePath, { flags: "a" }); + const active: ActiveResponseLog = { + stream, + pending: Promise.resolve(), + }; + + stream.on("error", (error) => { + this.options.logger?.error?.("[request-response-log] response log stream error:", error); + }); + + this.activeResponses.set(id, active); + return active; + } + + private async append(id: string, event: Record): Promise { + try { + const active = this.getOrCreate(id); + const line = JSON.stringify({ requestLogId: id, timestamp: new Date().toISOString(), ...event }) + "\n"; + active.pending = active.pending.then( + () => + new Promise((resolve, reject) => { + active.stream.write(line, (error) => { + if (error) reject(error); + else resolve(); + }); + }), + ); + await active.pending; + } catch (error) { + this.options.logger?.error?.("[request-response-log] failed to append response event:", error); + } + } + + private async close(id: string): Promise { + const active = this.activeResponses.get(id); + if (!active) return; + this.activeResponses.delete(id); + await active.pending; + await new Promise((resolve) => active.stream.end(resolve)); + } +} From d8385d5c2ef7a7c1d6a7ed3c4bf1d0d205af8123 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:40:16 +0800 Subject: [PATCH 26/42] verson bump --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f6b26e6..e2857aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "routstrd", - "version": "0.3.1", + "version": "0.3.3", "module": "src/index.ts", "type": "module", "private": false, @@ -24,7 +24,7 @@ }, "dependencies": { "@cashu/cashu-ts": "^4.3.0", - "@routstr/sdk": "file:../routstr-sdk", + "@routstr/sdk": "^0.3.11", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", From 9cc3b764836dd3dab4b19b784b3024c0b788722a Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:39:58 +0800 Subject: [PATCH 27/42] refactor(tui): separate async fetching from sync rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the monolithic render() into two distinct functions: - fetchData(): async background fetch that updates state and triggers a repaint, guarded against overlapping calls - render(): synchronous paint that reads current state and writes to stdout, safe to call from key handlers without blocking Run all four daemon calls concurrently via Promise.all to cut the blocked window. Remove redundant isDaemonRunning() checks from each fetch function in data.ts — the single check now lives in fetchData(). Key handlers now call the sync render() directly instead of void render(false), so scrolling and tab switching feel instant. --- src/tui/usage/app.ts | 154 +++++++++++++++++++++++------------------- src/tui/usage/data.ts | 12 ---- 2 files changed, 85 insertions(+), 81 deletions(-) diff --git a/src/tui/usage/app.ts b/src/tui/usage/app.ts index 8d95c96..6eb9424 100644 --- a/src/tui/usage/app.ts +++ b/src/tui/usage/app.ts @@ -49,10 +49,9 @@ export async function runUsageTui(): Promise { let clients: ClientInfo[] = []; let visibleTabs: Tab[] = getVisibleTabs(false); let refreshInterval: ReturnType | null = null; - let shouldUpdate = true; let autoRefresh = true; let cleanedUp = false; - let rendering = false; + let fetching = false; if (isInteractive) { stdout.write(enterAlternateScreen() + hideCursor()); @@ -80,123 +79,141 @@ export async function runUsageTui(): Promise { process.on("SIGINT", () => cleanup(0)); process.on("SIGTERM", () => cleanup(0)); - async function render(forceFetch = false) { - if (rendering) return; - rendering = true; - + /** + * Background fetch — async, never blocks rendering. Updates the state + * variables only on success, then triggers a repaint. Overlapping calls + * are skipped via the `fetching` guard so we don't race stale results. + */ + async function fetchData(): Promise { + if (fetching) return; + fetching = true; try { - const width = getWidth(); - const height = getHeight(); + const running = await isDaemonRunning(); + if (!running) { + stats = null; + } else { + // Fire all 4 fetches concurrently — cuts the blocked window significantly. + const [newStats, newBalance, newStatus, newClients] = await Promise.all([ + fetchUsageSummary(), + fetchBalance(), + fetchStatus(), + fetchClients(), + ]); + if (newStats) stats = newStats; + if (newBalance) balance = newBalance; + if (newStatus) status = newStatus; + if (newClients && newClients.length > 0) clients = newClients; - if (forceFetch || shouldUpdate) { - stats = await fetchUsageSummary(); - balance = await fetchBalance(); - status = await fetchStatus(); - clients = await fetchClients(); const npubsVisible = hasAnyNpubs(clients); visibleTabs = getVisibleTabs(npubsVisible); - // If current tab is npubs but it's no longer visible, fall back to clients if (currentTab === "npubs" && !npubsVisible) { currentTab = "clients"; vimState.scrollPos = 0; } - shouldUpdate = false; } - - if (!stats) { - stdout.write( - moveCursor(1, 1) + - eraseDown() + - `${COLORS.red}Error: Could not fetch usage data.${COLORS.reset}\n` + - `Make sure routstrd is running.\n` + - `\nPress Q to quit.` - ); - return; - } - - const content = renderTabContent(currentTab, stats, balance, status, width, clients); - 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}` : ""}`; - const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar(); - const chromeLines = chrome.split("\n").length - 1; - const footerSeparator = renderSeparator(width); - const footerLines = footerSeparator.split("\n").length - 1; - const contentViewportHeight = Math.max(1, height - chromeLines - footerLines - 1); - const visibleContent = applyScrollToContent(content, contentViewportHeight); - const footerBlock = (visibleContent ? "\n" : "") + footerSeparator + footer; - - stdout.write(moveCursor(1, 1) + eraseDown() + chrome + visibleContent + footerBlock); + render(); } finally { - rendering = false; + fetching = false; } } + /** + * Synchronous paint — never awaits I/O. Reads the current state variables + * and writes to stdout. Safe to call from key handlers without blocking. + */ + function render(): void { + const width = getWidth(); + const height = getHeight(); + + if (!stats) { + stdout.write( + moveCursor(1, 1) + + eraseDown() + + `${COLORS.red}Error: Could not fetch usage data.${COLORS.reset}\n` + + `Make sure routstrd is running.\n` + + `\nPress Q to quit.` + ); + return; + } + + const content = renderTabContent(currentTab, stats, balance, status, width, clients); + 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}` : ""}`; + const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar(); + const chromeLines = chrome.split("\n").length - 1; + const footerSeparator = renderSeparator(width); + const footerLines = footerSeparator.split("\n").length - 1; + const contentViewportHeight = Math.max(1, height - chromeLines - footerLines - 1); + const visibleContent = applyScrollToContent(content, contentViewportHeight); + const footerBlock = (visibleContent ? "\n" : "") + footerSeparator + footer; + + stdout.write(moveCursor(1, 1) + eraseDown() + chrome + visibleContent + footerBlock); + } + const handleKey = (key: string) => { if (vimState.isSearching) { if (key === "\x1b" || key === "\x1b[3~") { exitSearchMode(); - void render(false); + render(); return; } if (key === "\r" || key === "\n") { if (stats?.entries) performSearch(vimState.searchQuery, stats.entries); exitSearchMode(); - void render(false); + render(); return; } if (key === "\x7f" || key === "\x08") { vimState.searchQuery = vimState.searchQuery.slice(0, -1); if (stats?.entries) performSearch(vimState.searchQuery, stats.entries); - void render(false); + render(); return; } if (key === "\x03") { exitSearchMode(); - void render(false); + render(); return; } if (key.length === 1 && key.charCodeAt(0) >= 32 && key.charCodeAt(0) < 127) { vimState.searchQuery += key; if (stats?.entries) performSearch(vimState.searchQuery, stats.entries); - void render(false); + render(); } return; } if (key === "q" || key === "Q" || key === "\u0003") return cleanup(0); if (key === "r" || key === "R") { - shouldUpdate = true; - void render(true); + void fetchData(); return; } if (key === "a" || key === "A") { autoRefresh = !autoRefresh; - shouldUpdate = true; - void render(false); + render(); return; } if (key === "j" || key === "\x1b[B" || key === "\x1bOB") { scrollDown(); - void render(false); + render(); return; } if (key === "k" || key === "\x1b[A" || key === "\x1bOA") { scrollUp(); - void render(false); + render(); return; } if (key === "l" || key === "\x1b[C" || key === "\x1bOC") { const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab); currentTab = visibleTabs[(currentIdx + 1) % visibleTabs.length]!.id; vimState.scrollPos = 0; - void render(false); + render(); return; } if (key === "h" || key === "\x1b[D" || key === "\x1bOD") { const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab); currentTab = visibleTabs[(currentIdx - 1 + visibleTabs.length) % visibleTabs.length]!.id; vimState.scrollPos = 0; - void render(false); + render(); return; } @@ -204,54 +221,53 @@ export async function runUsageTui(): Promise { if (vimState.lastKey === "g" && Date.now() - vimState.lastKeyTime < 300) { scrollToBottom(); vimState.lastKey = ""; - void render(false); + render(); return; } vimState.lastKey = "g"; vimState.lastKeyTime = Date.now(); scrollToTop(); - void render(false); + render(); return; } - if (key === "\x02") { pageUp(); void render(false); return; } - if (key === "\x06") { pageDown(); void render(false); return; } - if (key === "\x15") { scrollUp(10); void render(false); return; } - if (key === "\x04") { scrollDown(10); void render(false); return; } - if (key === "\x1b[H" || key === "\x1b[1~" || key === "\x1bOH") { scrollToTop(); void render(false); return; } - if (key === "\x1b[F" || key === "\x1b[4~" || key === "\x1bOF") { scrollToBottom(); void render(false); return; } - if (key === "/") { startSearch(false); void render(false); return; } - if (key === "?") { startSearch(true); void render(false); return; } + if (key === "\x02") { pageUp(); render(); return; } + if (key === "\x06") { pageDown(); render(); return; } + if (key === "\x15") { scrollUp(10); render(); return; } + if (key === "\x04") { scrollDown(10); render(); return; } + if (key === "\x1b[H" || key === "\x1b[1~" || key === "\x1bOH") { scrollToTop(); render(); return; } + if (key === "\x1b[F" || key === "\x1b[4~" || key === "\x1bOF") { scrollToBottom(); render(); return; } + if (key === "/") { startSearch(false); render(); return; } + if (key === "?") { startSearch(true); render(); return; } if (key === "n") { if (vimState.searchReverse) prevSearchResult(stats?.entries.length || 0); else nextSearchResult(stats?.entries.length || 0); - void render(false); + render(); return; } if (key === "N") { if (vimState.searchReverse) nextSearchResult(stats?.entries.length || 0); else prevSearchResult(stats?.entries.length || 0); - void render(false); + render(); return; } - if (key === "\x1b") { scrollToTop(); void render(false); return; } + if (key === "\x1b") { scrollToTop(); render(); return; } const tab = visibleTabs.find((t) => t.key === key); if (tab) { currentTab = tab.id; vimState.scrollPos = 0; - void render(false); + render(); } }; if (isInteractive) stdin.on("data", handleKey); - await render(true); + await fetchData(); refreshInterval = setInterval(() => { if (autoRefresh) { - shouldUpdate = true; - void render(true); + void fetchData(); } }, 2000); } diff --git a/src/tui/usage/data.ts b/src/tui/usage/data.ts index 66821af..e58412a 100644 --- a/src/tui/usage/data.ts +++ b/src/tui/usage/data.ts @@ -26,9 +26,6 @@ export interface StatusInfo { export async function fetchStatus(): Promise { try { - const running = await isDaemonRunning(); - if (!running) return null; - const result = await callDaemon("/status"); if (result.error) return null; @@ -52,9 +49,6 @@ export async function fetchStatus(): Promise { export async function fetchBalance(): Promise { try { - const running = await isDaemonRunning(); - if (!running) return null; - const result = await callDaemon("/keys/balance"); if (result.error) return null; @@ -78,9 +72,6 @@ export async function fetchBalance(): Promise { export async function fetchUsageSummary(): Promise { try { - const running = await isDaemonRunning(); - if (!running) return null; - const tz = new Date().getTimezoneOffset(); const result = await callDaemon(`/usage/summary?tz=${tz}`); if (result.error) return null; @@ -123,9 +114,6 @@ export interface ClientInfo { export async function fetchClients(): Promise { try { - const running = await isDaemonRunning(); - if (!running) return []; - const result = await callDaemon("/clients"); if (result.error) return []; From a5be3b06e96c91d898e5e6989cc4dcef2faf38d1 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 22 Jun 2026 20:09:31 +0800 Subject: [PATCH 28/42] bump version to 0.3.4 and @routstr/sdk to 0.3.12 --- bun.lock | 176 ++++++++++++++++++++++++++++++++++++++++++--------- package.json | 4 +- 2 files changed, 149 insertions(+), 31 deletions(-) diff --git a/bun.lock b/bun.lock index eab41b4..37db574 100644 --- a/bun.lock +++ b/bun.lock @@ -131,57 +131,91 @@ "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.61.1", "", { "os": "android", "cpu": "arm" }, "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA=="], + "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.61.1", "", { "os": "android", "cpu": "arm64" }, "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.61.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.61.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.61.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.61.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.61.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.61.1", "", { "os": "linux", "cpu": "arm" }, "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.61.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.61.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.61.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.61.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.3", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.61.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.61.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.61.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.61.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.61.1", "", { "os": "none", "cpu": "arm64" }, "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.61.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.61.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.61.1", "", { "os": "win32", "cpu": "x64" }, "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.61.1", "", { "os": "win32", "cpu": "x64" }, "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="], - "@routstr/sdk": ["@routstr/sdk@file:../routstr-sdk", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "rxjs": "^7.8.1", "zustand": "^5.0.5" }, "devDependencies": { "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.0.0", "joycon": "^3.1.1", "tsup": "^8.5.1", "typescript": "^5.0.0" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], + + "@routstr/sdk": ["@routstr/sdk@file:../routstr-sdk", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "rxjs": "^7.8.1", "zustand": "^5.0.5" }, "devDependencies": { "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.0.0", "joycon": "^3.1.1", "tsup": "^8.5.1", "typescript": "^5.0.0", "vitest": "^4.1.9" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }], "@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="], @@ -189,6 +223,8 @@ "@scure/bip39": ["@scure/bip39@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@tursodatabase/database": ["@tursodatabase/database@0.2.2", "", { "dependencies": { "@tursodatabase/database-common": "^0.2.2" }, "optionalDependencies": { "@tursodatabase/database-darwin-arm64": "0.2.2", "@tursodatabase/database-linux-arm64-gnu": "0.2.2", "@tursodatabase/database-linux-x64-gnu": "0.2.2", "@tursodatabase/database-win32-x64-msvc": "0.2.2" } }, "sha512-BlvoyiwIWIIQA65KEYlCqLDpRKIOM1AN99tBMjz1B+ydO9h0cDb+lCbfBf6f7+Lk696KB1ODpePiF5EfGm9qpw=="], "@tursodatabase/database-common": ["@tursodatabase/database-common@0.2.2", "", {}, "sha512-cSNpms6MIaRj29B37XzIu9yGbea0HSGDupZs8QrMxR3rKqgHJIfY04ysqDD/4lS8TNIImPlPZrZgqQz+b/FoKQ=="], @@ -211,6 +247,10 @@ "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], @@ -219,7 +259,21 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], + + "@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], + + "@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], + + "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], + + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -237,6 +291,8 @@ "applesauce-wallet-connect": ["applesauce-wallet-connect@6.0.0", "", { "dependencies": { "@noble/hashes": "^1.7.1", "applesauce-common": "^6.0.0", "applesauce-core": "^6.0.0", "rxjs": "^7.8.1" } }, "sha512-rdVvc2NIa6VqnJh2JucyMjhA+S9bohPfXxChJtM4TkWwTFxkhkao9W+7Fuoe9ctelubGKXBbmtOSX3WowjgUOg=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "better-sqlite3": ["better-sqlite3@12.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ=="], @@ -255,6 +311,8 @@ "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], @@ -271,6 +329,8 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -289,10 +349,16 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], + "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -333,6 +399,30 @@ "light-bolt11-decoder": ["light-bolt11-decoder@3.2.0", "", { "dependencies": { "@scure/base": "1.1.1" } }, "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -371,6 +461,8 @@ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], @@ -393,6 +485,8 @@ "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], @@ -415,7 +509,9 @@ "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - "rollup": ["rollup@4.61.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.1", "@rollup/rollup-android-arm64": "4.61.1", "@rollup/rollup-darwin-arm64": "4.61.1", "@rollup/rollup-darwin-x64": "4.61.1", "@rollup/rollup-freebsd-arm64": "4.61.1", "@rollup/rollup-freebsd-x64": "4.61.1", "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", "@rollup/rollup-linux-arm-musleabihf": "4.61.1", "@rollup/rollup-linux-arm64-gnu": "4.61.1", "@rollup/rollup-linux-arm64-musl": "4.61.1", "@rollup/rollup-linux-loong64-gnu": "4.61.1", "@rollup/rollup-linux-loong64-musl": "4.61.1", "@rollup/rollup-linux-ppc64-gnu": "4.61.1", "@rollup/rollup-linux-ppc64-musl": "4.61.1", "@rollup/rollup-linux-riscv64-gnu": "4.61.1", "@rollup/rollup-linux-riscv64-musl": "4.61.1", "@rollup/rollup-linux-s390x-gnu": "4.61.1", "@rollup/rollup-linux-x64-gnu": "4.61.1", "@rollup/rollup-linux-x64-musl": "4.61.1", "@rollup/rollup-openbsd-x64": "4.61.1", "@rollup/rollup-openharmony-arm64": "4.61.1", "@rollup/rollup-win32-arm64-msvc": "4.61.1", "@rollup/rollup-win32-ia32-msvc": "4.61.1", "@rollup/rollup-win32-x64-gnu": "4.61.1", "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA=="], + "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], + + "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], @@ -425,12 +521,20 @@ "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -449,10 +553,14 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], @@ -471,10 +579,16 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], + + "vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -491,7 +605,7 @@ "@routstr/sdk/@cashu/cashu-ts": ["@cashu/cashu-ts@3.7.0", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-Jy4Hek+Wouh94xS4InmTJRLpBPXA6fddVOAHR5lfNyTsCk9r3desY0vnwUGvOZBZWsSKZXedqdD/YpjWb3zngQ=="], - "@routstr/sdk/@types/node": ["@types/node@22.19.20", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw=="], + "@routstr/sdk/@types/node": ["@types/node@22.20.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g=="], "@scure/bip39/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], @@ -525,8 +639,12 @@ "nostr-tools/@scure/bip32": ["@scure/bip32@2.0.1", "", { "dependencies": { "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA=="], + "postcss/nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="], + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "vitest/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + "@routstr/sdk/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "applesauce-common/applesauce-core/nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="], diff --git a/package.json b/package.json index e2857aa..45d4708 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "routstrd", - "version": "0.3.3", + "version": "0.3.4", "module": "src/index.ts", "type": "module", "private": false, @@ -24,7 +24,7 @@ }, "dependencies": { "@cashu/cashu-ts": "^4.3.0", - "@routstr/sdk": "^0.3.11", + "@routstr/sdk": "file:../routstr-sdk", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", From 3c6161d9bf1a177c38af2a40b8bedcd5bfd305bd Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sun, 28 Jun 2026 10:33:52 +0800 Subject: [PATCH 29/42] fix: add periodic Nostr event refresh to daemon Calls refreshNostrEvents() after initial bootstrap and in the 21-minute recurring job, covering provider discovery (38421) and lgtm reviews (38425) which were previously only refreshed on manual trigger. --- src/daemon/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 9875400..6f885e2 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -176,6 +176,13 @@ async function main(): Promise { ); refreshInterval = setInterval(async () => { + logger.log("Running scheduled Nostr event refresh..."); + try { + await modelManager.refreshNostrEvents(); + } catch (error) { + logger.error("Scheduled Nostr event refresh failed:", error); + } + logger.log("Running scheduled model refresh..."); try { await refreshModelsAndIntegrations(getRoutstr21Models, updatedConfig, "Scheduled"); @@ -268,6 +275,10 @@ async function main(): Promise { // Start the recurring model refresh job after initial bootstrap void ensureProvidersBootstrapped() .then(async () => { + // Catch up on any Nostr events published since last run + logger.log("Running initial Nostr event refresh..."); + await modelManager.refreshNostrEvents(); + startModelRefreshJob(); startRefundJob(); // Run an immediate refresh to populate models right away From b2738109e37f9debd844d63881f5be46770cb5a7 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sat, 4 Jul 2026 21:11:56 +0530 Subject: [PATCH 30/42] Sync daemon with SDK discovery adapter API --- src/daemon/http/index.ts | 3 --- src/daemon/index.ts | 7 ++----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index b66c3fa..96b4d59 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -37,7 +37,6 @@ type DaemonDeps = { walletClient: CocodClient; walletAdapter: any; storageAdapter: any; - providerRegistry: any; discoveryAdapter: any; modelManager: any; ensureProvidersBootstrapped: () => Promise; @@ -292,7 +291,6 @@ export function createDaemonRequestHandler(deps: { walletClient: CocodClient; walletAdapter: any; storageAdapter: any; - providerRegistry: any; discoveryAdapter: any; modelManager: any; ensureProvidersBootstrapped: () => Promise; @@ -1268,7 +1266,6 @@ export function createDaemonRequestHandler(deps: { headers: incomingHeaders, walletAdapter: deps.walletAdapter, storageAdapter: deps.storageAdapter, - providerRegistry: deps.providerRegistry, discoveryAdapter: deps.discoveryAdapter, modelManager: deps.modelManager, debugLevel: "DEBUG", diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 6f885e2..f1d0de4 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -39,7 +39,6 @@ import { createBunSqliteDriver, createBunSqliteUsageTrackingDriver, createShardedDiscoveryAdapter, - createProviderRegistryFromDiscoveryAdapter, } from "@routstr/sdk/storage/bun"; import { createWalletAdapter } from "./wallet"; import type { AutoRefillConfig } from "./wallet/auto-refill"; @@ -82,7 +81,6 @@ async function main(): Promise { }); const discoveryAdapter = await createShardedDiscoveryAdapter({ driver: sqliteDriver }); - const providerRegistry = createProviderRegistryFromDiscoveryAdapter(discoveryAdapter, daemonSdkLogger); const storageAdapter = createStorageAdapterFromStore(store); const modelManager = new ModelManager(discoveryAdapter, { logger: daemonSdkLogger, @@ -91,7 +89,7 @@ async function main(): Promise { nostrRelays: config.relays, }); // Create shared ProviderManager for consistent failure tracking across all requests - const providerManager = new ProviderManager(providerRegistry, store, daemonSdkLogger); + const providerManager = new ProviderManager(discoveryAdapter, store, daemonSdkLogger); const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, refreshProvidersAndModels } = createModelService(modelManager, store); @@ -123,7 +121,7 @@ async function main(): Promise { const refundClient = new RoutstrClient( walletAdapter, storageAdapter, - providerRegistry, + discoveryAdapter, "min", "apikeys", { logger: daemonSdkLogger }, @@ -139,7 +137,6 @@ async function main(): Promise { walletClient, walletAdapter, storageAdapter, - providerRegistry, discoveryAdapter, modelManager, ensureProvidersBootstrapped, From 1ff3b9a4e1f0f1694bc0ed31fe342aecfd10d284 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:27:20 +0530 Subject: [PATCH 31/42] Fix renderToday definite-assignment and usage-summary stale fixtures renderToday declared todayStats/recentDays/hourlyMap with let but only assigned inside if (stats.summary); TypeScript flagged them as used before assignment (TS2454). Initialize with sensible defaults so the function degrades gracefully when summary is missing. The usage-summary tz-bucketing test used hardcoded May 2026 timestamps with a comment dated 2026-06-02. Those entries aged out of getUsageSummary's 30-day rolling window, so days came back empty. Recompute timestamps relative to now and assert on dynamically-derived local-day date strings. --- src/daemon/http/usage-summary.test.ts | 39 ++++++++++++++++++--------- src/tui/usage/render.ts | 19 ++++++++----- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/daemon/http/usage-summary.test.ts b/src/daemon/http/usage-summary.test.ts index 59f97df..f97a201 100644 --- a/src/daemon/http/usage-summary.test.ts +++ b/src/daemon/http/usage-summary.test.ts @@ -6,15 +6,27 @@ import { getUsageSummary, __resetUsageSummaryCacheForTest } from "./usage-summar // ─── Test fixtures ──────────────────────────────────────────────────────────── // -// All timestamps fall within 30 days of 2026-06-02 (today as of this writing). +// All timestamps are computed relative to "now" so the fixtures never age +// out of getUsageSummary's 30-day rolling window. We anchor to a recent UTC +// midnight ~10 days ago and keep the same hour-of-day structure the +// tz-bucketing assertions depend on. +// // tz = 300 (UTC-5 / EST). After shifting by -300min, UTC dates become: -// d1: 2026-05-20T10:00Z → shifted 2026-05-20T05:00Z → local day "2026-05-20", hour 05 -// d2: 2026-05-21T03:00Z → shifted 2026-05-20T22:00Z → local day "2026-05-20", hour 22 -// d3: 2026-05-21T06:00Z → shifted 2026-05-21T01:00Z → local day "2026-05-21", hour 01 +// d1: 10:00Z → local 05:00 → local day "" +// d2: 03:00Z → local 22:00 → local day "" +// d3: 06:00Z → local 01:00 → local day "" -const D1 = new Date("2026-05-20T10:00:00Z").getTime(); // local 2026-05-20, hour 05 -const D2 = new Date("2026-05-21T03:00:00Z").getTime(); // local 2026-05-20, hour 22 -const D3 = new Date("2026-05-21T06:00:00Z").getTime(); // local 2026-05-21, hour 01 +const _BASE_UTC_MIDNIGHT = Math.floor(Date.now() / 86_400_000) * 86_400_000; +const _ANCHOR_MS = _BASE_UTC_MIDNIGHT - 10 * 86_400_000; // 10 days ago UTC +const D1 = _ANCHOR_MS + 10 * 3_600_000; // 10:00Z → local day +const D2 = _ANCHOR_MS + 1 * 86_400_000 + 3 * 3_600_000; // 03:00Z next day → local day +const D3 = _ANCHOR_MS + 1 * 86_400_000 + 6 * 3_600_000; // 06:00Z next day → local day + +/** Compute the local-day date string the same way the SDK's day grouping does. */ +function localDayDateStr(ts: number, tzOffsetMinutes: number): string { + const d = new Date(ts - tzOffsetMinutes * 60_000); + return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`; +} const BASE_ENTRY: Omit = { baseUrl: "https://api.openai.com/", @@ -152,23 +164,24 @@ describe("getUsageSummary", () => { const summary = await getUsageSummary(driver, CLIENTS, TZ); const { days } = summary; - // D1 (10:00Z) → local 2026-05-20, D2 (03:00Z) → local 2026-05-20, D3 (06:00Z) → local 2026-05-21 - // So: day 2026-05-20 has e1+e2, day 2026-05-21 has e3+e4 + // D1 (10:00Z) and D2 (next day 03:00Z) both bucket to local day ; + // D3 (next day 06:00Z) buckets to local day . + // So: day has e1+e2, day has e3+e4 expect(days.length).toBeGreaterThanOrEqual(2); - // Most-recent-first: 2026-05-21 first - expect(days[0]!.date).toBe("2026-05-21"); + // Most-recent-first: first + expect(days[0]!.date).toBe(localDayDateStr(D3, TZ)); expect(days[0]!.requests).toBe(2); // e3, e4 expect(days[0]!.satsCost).toBe(8 + 50); - expect(days[1]!.date).toBe("2026-05-20"); + expect(days[1]!.date).toBe(localDayDateStr(D1, TZ)); expect(days[1]!.requests).toBe(2); // e1, e2 expect(days[1]!.satsCost).toBe(10 + 20); }); it("returns empty hoursToday (test entries are not today)", async () => { const summary = await getUsageSummary(driver, CLIENTS, TZ); - // Test entries are in May 2026, not today (June 2026) + // Test entries are ~10 days ago, not today expect(summary.hoursToday).toHaveLength(0); }); diff --git a/src/tui/usage/render.ts b/src/tui/usage/render.ts index 2fe7b41..727598e 100644 --- a/src/tui/usage/render.ts +++ b/src/tui/usage/render.ts @@ -278,18 +278,25 @@ export function renderToday(stats: UsageStats, width: number): string { const _now = new Date(); const todayDateStr = `${_now.getFullYear()}-${String(_now.getMonth() + 1).padStart(2, "0")}-${String(_now.getDate()).padStart(2, "0")}`; - let todayStats: { date: string; requests: number; satsCost: number; promptTokens: number; completionTokens: number; totalTokens: number }; - let recentDays: Array<{ date: string; requests: number; satsCost: number; totalTokens: number; promptTokens: number; completionTokens: number }>; - let hourlyMap: Map; + let todayStats: { date: string; requests: number; satsCost: number; promptTokens: number; completionTokens: number; totalTokens: number } = { + date: todayDateStr, + requests: 0, + satsCost: 0, + promptTokens: 0, + completionTokens: 0, + totalTokens: 0, + }; + let recentDays: Array<{ date: string; requests: number; satsCost: number; totalTokens: number; promptTokens: number; completionTokens: number }> = []; + let hourlyMap: Map = new Map(); if (stats.summary) { // Use server-aggregated data const { days, hoursToday } = stats.summary; // days[0] is most-recent-first; check if it's today const todayDayStat = days[0]?.date === todayDateStr ? days[0] : undefined; - 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 }; + if (todayDayStat) { + todayStats = { date: todayDayStat.date, requests: todayDayStat.requests, satsCost: todayDayStat.satsCost, promptTokens: todayDayStat.promptTokens, completionTokens: todayDayStat.completionTokens, totalTokens: todayDayStat.totalTokens }; + } // Exclude today by date (it has its own box) rather than by position — // days[0] is only today when there was activity today. recentDays = days.filter((d) => d.date !== todayDateStr).slice(0, 6); From 1a06093b961124b52a598a65d207cde97e27eccf Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:37:34 +0530 Subject: [PATCH 32/42] bump version to 0.3.5, update @routstr/sdk to 0.3.14 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 45d4708..f0245de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "routstrd", - "version": "0.3.4", + "version": "0.3.5", "module": "src/index.ts", "type": "module", "private": false, @@ -24,7 +24,7 @@ }, "dependencies": { "@cashu/cashu-ts": "^4.3.0", - "@routstr/sdk": "file:../routstr-sdk", + "@routstr/sdk": "^0.3.14", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", From f58e807807608e04d725dae58b5cf98d95515e46 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:27:00 +0800 Subject: [PATCH 33/42] Add 'update' command to upgrade routstrd and cocod globally --- package.json | 2 +- src/cli.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f0245de..042aeb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "routstrd", - "version": "0.3.5", + "version": "0.3.6", "module": "src/index.ts", "type": "module", "private": false, diff --git a/src/cli.ts b/src/cli.ts index 24f2e8c..586b000 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -235,6 +235,37 @@ program .description("Routstr daemon - Manage routstr processes") .version(packageJson.version, "--version", "output the version number"); +program + .command("update") + .description("Update routstrd and cocod to the latest versions") + .action(async () => { + console.log("Updating routstrd..."); + const routstrdProc = Bun.spawn(["bun", "install", "-g", "routstrd"], { + stdout: "inherit", + stderr: "inherit", + }); + const routstrdCode = await routstrdProc.exited; + if (routstrdCode !== 0) { + console.error("Failed to update routstrd."); + process.exit(1); + } + console.log("routstrd updated successfully.\n"); + + console.log("Updating cocod..."); + const cocodProc = Bun.spawn(["bun", "install", "-g", "@routstr/cocod"], { + stdout: "inherit", + stderr: "inherit", + }); + const cocodCode = await cocodProc.exited; + if (cocodCode !== 0) { + console.error("Failed to update cocod."); + process.exit(1); + } + console.log("cocod updated successfully.\n"); + + console.log("Both routstrd and cocod have been updated!"); + }); + program .command("refund") .description("Refund pending tokens and API keys to a specified mint") From cf1ca5941acb616fee261de8542b40d29677a8ed Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:39:31 +0800 Subject: [PATCH 34/42] feat(update): restart daemons after update The update command previously only downloaded and installed new binaries for routstrd and cocod without restarting the running daemons, so updates would not take effect until a manual restart. - Add restartDaemonsAfterUpdate() helper that gracefully stops and restarts both daemons after a successful update - routstrd: uses POST /stop (drains active connections), polls for shutdown, then calls startDaemon() with configured port/provider - cocod: runs 'cocod stop', then spawns 'cocod daemon' detached, polls 'cocod ping' until it comes back up - Skips restart for daemons that weren't running - Skips routstrd daemon restart when using a remote daemon - Collects and reports failures without rolling back the update - Bump @routstr/sdk to 0.3.15 - Bump routstrd version to 0.3.7 Closes nostr task: update-restart --- package.json | 4 +- src/cli.ts | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 042aeb7..6bdf901 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "routstrd", - "version": "0.3.6", + "version": "0.3.7", "module": "src/index.ts", "type": "module", "private": false, @@ -24,7 +24,7 @@ }, "dependencies": { "@cashu/cashu-ts": "^4.3.0", - "@routstr/sdk": "^0.3.14", + "@routstr/sdk": "^0.3.15", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", diff --git a/src/cli.ts b/src/cli.ts index 586b000..06cdd90 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -103,6 +103,130 @@ async function installCocodOrExit(): Promise { console.log("cocod installed successfully."); } +/** + * Restart the routstrd and cocod daemons after an update so the new + * binaries take effect immediately. Failures are collected and reported + * but never roll back the update itself. + */ +async function restartDaemonsAfterUpdate(): Promise { + const config = await loadConfig(); + const isRemote = !!config.daemonUrl; + const failures: string[] = []; + + // --- routstrd daemon --- + if (isRemote) { + console.log("\nUsing remote daemon — skipping routstrd daemon restart."); + } else { + try { + const wasRunning = await isDaemonRunning(); + if (!wasRunning) { + console.log("\nroutstrd daemon was not running — skipping restart."); + } else { + console.log("\nRestarting routstrd daemon..."); + + // Graceful stop — the /stop endpoint closes the HTTP server + // (draining active connections) then exits. + await callDaemon("/stop", { method: "POST" }); + + for (let i = 0; i < 50; i++) { + await new Promise((resolve) => setTimeout(resolve, 100)); + if (!(await isDaemonRunning())) break; + } + + if (await isDaemonRunning()) { + throw new Error("routstrd did not stop within 5 seconds"); + } + console.log("routstrd daemon stopped."); + + console.log("Starting routstrd daemon..."); + await startDaemon({ + port: String(config.port || 8008), + provider: config.provider || undefined, + }); + console.log("routstrd daemon restarted."); + } + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + failures.push(`routstrd daemon: ${msg}`); + } + } + + // --- cocod daemon --- + try { + const cocodExecutable = resolveCocodExecutable(config.cocodPath); + + // Check whether cocod is currently running. + const pingProc = Bun.spawn([cocodExecutable, "ping"], { + stdout: "ignore", + stderr: "ignore", + }); + const cocodWasRunning = (await pingProc.exited) === 0; + + if (!cocodWasRunning) { + console.log("\ncocod daemon was not running — skipping restart."); + } else { + console.log("\nRestarting cocod daemon..."); + + const stopProc = Bun.spawn([cocodExecutable, "stop"], { + stdout: "inherit", + stderr: "inherit", + }); + const stopCode = await stopProc.exited; + if (stopCode !== 0) { + throw new Error(`cocod stop exited with code ${stopCode}`); + } + console.log("cocod daemon stopped."); + + // Start cocod in the background (detached, like the wallet client does). + const env = { ...process.env }; + const startProc = Bun.spawn([cocodExecutable, "daemon"], { + stdout: "ignore", + stderr: "ignore", + stdin: "ignore", + detached: true, + env, + }); + startProc.unref?.(); + + // Poll until cocod responds to ping (max ~10 s). + let started = false; + for (let i = 0; i < 100; i++) { + await new Promise((resolve) => setTimeout(resolve, 100)); + const poll = Bun.spawn([cocodExecutable, "ping"], { + stdout: "ignore", + stderr: "ignore", + }); + if ((await poll.exited) === 0) { + started = true; + break; + } + } + + if (!started) { + throw new Error("cocod did not come back up within 10 seconds"); + } + console.log("cocod daemon restarted."); + } + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + failures.push(`cocod daemon: ${msg}`); + } + + // --- report --- + if (failures.length > 0) { + console.error("\n⚠ Some daemons failed to restart:"); + for (const f of failures) { + console.error(` - ${f}`); + } + console.error( + "The update was applied but may not take effect until daemons are manually restarted.", + ); + process.exit(1); + } + + console.log("\n✓ All daemons restarted successfully."); +} + async function requireLocalDaemon(): Promise { const config = await loadConfig(); if (config.daemonUrl) { @@ -264,6 +388,9 @@ program console.log("cocod updated successfully.\n"); console.log("Both routstrd and cocod have been updated!"); + + // Restart daemons so the new binaries take effect immediately. + await restartDaemonsAfterUpdate(); }); program From 60bc8b4cb2f9aa8f7d278ab843ff2dfe29968f1e Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 9 Jul 2026 00:07:13 +0800 Subject: [PATCH 35/42] fix: capture daemon crash logs and prevent silent crashes - Redirect detached daemon stdout/stderr to ~/.routstrd/debug.log instead of ignoring them, so uncaught exception stack traces are no longer lost - Add process-level uncaughtException/unhandledRejection handlers that log to the file logger before the process dies - Wrap setInterval async callbacks (model refresh + refund jobs) in IIFE catch chains so rejected promises can't escape and kill the process silently --- src/daemon/index.ts | 117 +++++++++++++++++++++++++------------------- src/start-daemon.ts | 8 ++- 2 files changed, 72 insertions(+), 53 deletions(-) diff --git a/src/daemon/index.ts b/src/daemon/index.ts index f1d0de4..c44e605 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -49,6 +49,17 @@ import { FileRequestResponseLogSink } from "./request-response-log-sink"; import { refreshModelsAndIntegrations } from "../integrations"; import { RoutstrClient } from "@routstr/sdk"; +// Global error handlers — the daemon is spawned detached with stdout/stderr +// redirected to a file, so without these, uncaught async errors would kill +// the process silently. Log to the file logger before exiting. +process.on("uncaughtException", (error) => { + logger.error("UNCAUGHT EXCEPTION:", error); +}); + +process.on("unhandledRejection", (reason) => { + logger.error("UNHANDLED REJECTION:", reason); +}); + async function main(): Promise { const args = parseArgs(process.argv); const config = await loadDaemonConfig(); @@ -172,20 +183,22 @@ async function main(): Promise { `Starting recurring model refresh job (every ${REFRESH_INTERVAL_MS / 1000 / 60 / 60} hours)`, ); - refreshInterval = setInterval(async () => { - logger.log("Running scheduled Nostr event refresh..."); - try { - await modelManager.refreshNostrEvents(); - } catch (error) { - logger.error("Scheduled Nostr event refresh failed:", error); - } + refreshInterval = setInterval(() => { + (async () => { + logger.log("Running scheduled Nostr event refresh..."); + try { + await modelManager.refreshNostrEvents(); + } catch (error) { + logger.error("Scheduled Nostr event refresh failed:", error); + } - logger.log("Running scheduled model refresh..."); - try { - await refreshModelsAndIntegrations(getRoutstr21Models, updatedConfig, "Scheduled"); - } catch (error) { - logger.error("Scheduled model refresh failed:", error); - } + logger.log("Running scheduled model refresh..."); + try { + await refreshModelsAndIntegrations(getRoutstr21Models, updatedConfig, "Scheduled"); + } catch (error) { + logger.error("Scheduled model refresh failed:", error); + } + })().catch((error) => logger.error("Model refresh interval escaped:", error)); }, REFRESH_INTERVAL_MS); }; @@ -206,46 +219,48 @@ async function main(): Promise { `Starting recurring refund job (every ${REFUND_INTERVAL_MS / 1000 / 60} minutes)`, ); - refundInterval = setInterval(async () => { - logger.log("Running scheduled refund..."); - try { - const state = store.getState() as any; - const pendingDistribution = (state.cachedTokens || []).map( - (t: { baseUrl: string; balance?: number }) => ({ - baseUrl: t.baseUrl, - amount: t.balance || 0, - }), - ); - const apiKeysStored = (state.apiKeys || []).map( - (k: { baseUrl: string; balance?: number }) => ({ - baseUrl: k.baseUrl, - amount: k.balance || 0, - }), - ); + refundInterval = setInterval(() => { + (async () => { + logger.log("Running scheduled refund..."); + try { + const state = store.getState() as any; + const pendingDistribution = (state.cachedTokens || []).map( + (t: { baseUrl: string; balance?: number }) => ({ + baseUrl: t.baseUrl, + amount: t.balance || 0, + }), + ); + const apiKeysStored = (state.apiKeys || []).map( + (k: { baseUrl: string; balance?: number }) => ({ + baseUrl: k.baseUrl, + amount: k.balance || 0, + }), + ); - if (pendingDistribution.length === 0 && apiKeysStored.length === 0) { - logger.log("No pending tokens to refund."); - return; + if (pendingDistribution.length === 0 && apiKeysStored.length === 0) { + logger.log("No pending tokens to refund."); + return; + } + + const mintUrl = walletAdapter.getActiveMintUrl(); + if (!mintUrl) { + logger.log("No active mint URL for refund."); + return; + } + + const spender = refundClient.getCashuSpender(); + const results = await spender.refundProviders(mintUrl); + + const successCount = results.filter( + (r: { success: boolean }) => r.success, + ).length; + logger.log( + `Scheduled refund completed: ${successCount}/${results.length} providers refunded.`, + ); + } catch (error) { + logger.error("Scheduled refund failed:", error); } - - const mintUrl = walletAdapter.getActiveMintUrl(); - if (!mintUrl) { - logger.log("No active mint URL for refund."); - return; - } - - const spender = refundClient.getCashuSpender(); - const results = await spender.refundProviders(mintUrl); - - const successCount = results.filter( - (r: { success: boolean }) => r.success, - ).length; - logger.log( - `Scheduled refund completed: ${successCount}/${results.length} providers refunded.`, - ); - } catch (error) { - logger.error("Scheduled refund failed:", error); - } + })().catch((error) => logger.error("Refund interval escaped:", error)); }, REFUND_INTERVAL_MS); }; diff --git a/src/start-daemon.ts b/src/start-daemon.ts index 9d9ea8f..f807886 100644 --- a/src/start-daemon.ts +++ b/src/start-daemon.ts @@ -1,8 +1,10 @@ +import { openSync } from "fs"; import { logger } from "./utils/logger"; import { CONFIG_DIR, LOGS_DIR } from "./utils/config"; import { withCrossProcessLock } from "./utils/process-lock"; const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`; +const DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`; async function isDaemonHealthy(port: string): Promise { const controller = new AbortController(); @@ -42,9 +44,11 @@ async function startDaemonUnlocked( const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname; const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")}`; + const debugLogFd = openSync(DEBUG_LOG_PATH, "a"); + const proc = Bun.spawn(["sh", "-c", shellCmd], { - stdout: "ignore", - stderr: "ignore", + stdout: debugLogFd, + stderr: debugLogFd, stdin: "ignore", detached: true, }); From 9e8f8178a9106a4f0bfdd984005cd5ced7954f02 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:27:30 +0530 Subject: [PATCH 36/42] chore: bump version to 0.3.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6bdf901..318036c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "routstrd", - "version": "0.3.7", + "version": "0.3.8", "module": "src/index.ts", "type": "module", "private": false, From a7de2013303bbbfb678793fb1ade8a9d2c95faec Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:57:30 +0530 Subject: [PATCH 37/42] chore: bump version to 0.3.9 --- bun.lock | 340 ++++++--------------------------------------------- package.json | 4 +- 2 files changed, 36 insertions(+), 308 deletions(-) diff --git a/bun.lock b/bun.lock index 37db574..3ef2700 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "name": "routstrd", "dependencies": { "@cashu/cashu-ts": "^4.3.0", - "@routstr/sdk": "file:../routstr-sdk", + "@routstr/sdk": "^0.3.16", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", @@ -25,6 +25,12 @@ }, }, "packages": { + "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.30" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A6qfyaVs4lxmRxRux6U3ViOa8mMbsSd0OaHghpei2MpiBT6791J4zFH5MN7kaW1tLfQ246rSC2DVTMOavsycjQ=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.30", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VO7I+vPffqI5sMnPoUq5DCSqKIgQIk/naJWRdQVpz2ma2zoprC/lqiJiUEl2s6DfvTD76TbhD3q39ROjlA6rGw=="], + "@cashu/cashu-ts": ["@cashu/cashu-ts@4.5.1", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-AGB7wh1SC0iANRSsdwM091eoS1uVhSf/Qf2VM3plw8EzlYjiFkwSa9X0Nh6NpsE1VVeAuPpNInvpznvElp2Q0A=="], "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], @@ -33,65 +39,11 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + "@freedomofpress/crypto-browser": ["@freedomofpress/crypto-browser@0.1.7", "", { "dependencies": { "@noble/curves": "^1.6.0" } }, "sha512-zjWmZDKdAu8g0Zq1IjBQ+sKQ/NpfzStBDFjy/qHUSMVEL4wNlNGtA7lhtw8v8asXa0yqF2QTYQ3rq6xCTQeADw=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + "@freedomofpress/sigstore-browser": ["@freedomofpress/sigstore-browser@0.1.13", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7", "@freedomofpress/tuf-browser": "^0.1.11", "@noble/curves": "^2.0.1" } }, "sha512-3YfmP9JQ5h8CAO/vKJEGYrFdzss6xWxYi5ZkbR4KoHlxIaLGrnu+5TQDoZVWhuhyfDBdLQqj6ypdN0W6PsH3Jw=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@freedomofpress/tuf-browser": ["@freedomofpress/tuf-browser@0.1.11", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7" } }, "sha512-d76ohB/AS5+zI+lnbiFMX/BIK3nT18hZ8q3Na6X4vyYaSYjXkeknzhAnbx1da+8ObWLwsaZLue46haEch28qtQ=="], "@libsql/client": ["@libsql/client@0.15.15", "", { "dependencies": { "@libsql/core": "^0.15.14", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w=="], @@ -131,91 +83,11 @@ "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], - "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], + "@noble/post-quantum": ["@noble/post-quantum@0.6.1", "", { "dependencies": { "@noble/ciphers": "~2.2.0", "@noble/curves": "~2.2.0", "@noble/hashes": "~2.2.0" } }, "sha512-+pormrDZwjRw05U8ADK4JpHejo87+gBd+muRBB/ozztH5yhDLMDF4jHQWN3NQQAsu1zBNPWTG0ZwVI0CR29H0A=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="], + "@panva/hpke-noble": ["@panva/hpke-noble@1.1.2", "", { "dependencies": { "@noble/ciphers": "^2.2.0", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@noble/post-quantum": "^0.6.1" }, "peerDependencies": { "hpke": "^1.0.0" } }, "sha512-Gr/jKr/ErpRow8aHK+SAp0sAaWy7iJGHlFsXsdTjD8ULcN4vvsVVWR3fiD6Ogf+VoSB28XY/xWRDUEIGXw+9aA=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="], - - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg=="], - - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g=="], - - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw=="], - - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw=="], - - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q=="], - - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg=="], - - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg=="], - - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg=="], - - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow=="], - - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg=="], - - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.3", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg=="], - - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g=="], - - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA=="], - - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="], - - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="], - - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], - - "@routstr/sdk": ["@routstr/sdk@file:../routstr-sdk", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "rxjs": "^7.8.1", "zustand": "^5.0.5" }, "devDependencies": { "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.0.0", "joycon": "^3.1.1", "tsup": "^8.5.1", "typescript": "^5.0.0", "vitest": "^4.1.9" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }], + "@routstr/sdk": ["@routstr/sdk@0.3.16", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "ehbp": "^0.2.3", "rxjs": "^7.8.1", "tinfoil": "^1.1.6", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-c5DMRcixvs4fghdYvikBAB+xQ4C9BPg71PRsPn8ZJ+ZxBgr9v5Xm1TXHauB/SpgmowccamXJ5Hd0rB9Ke5MsCw=="], "@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="], @@ -225,6 +97,8 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@tinfoilsh/verifier": ["@tinfoilsh/verifier@1.1.6", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7", "@freedomofpress/sigstore-browser": "^0.1.13", "@freedomofpress/tuf-browser": "^0.1.11" } }, "sha512-egZlLFU98kh48GKV5B8Gh1abGHCUGEcSijWmTv3hn5lqAMXdLsqyehKDaUen8/98CrNxiOP+mzeqVbKSyAs5ZQ=="], + "@tursodatabase/database": ["@tursodatabase/database@0.2.2", "", { "dependencies": { "@tursodatabase/database-common": "^0.2.2" }, "optionalDependencies": { "@tursodatabase/database-darwin-arm64": "0.2.2", "@tursodatabase/database-linux-arm64-gnu": "0.2.2", "@tursodatabase/database-linux-x64-gnu": "0.2.2", "@tursodatabase/database-win32-x64-msvc": "0.2.2" } }, "sha512-BlvoyiwIWIIQA65KEYlCqLDpRKIOM1AN99tBMjz1B+ydO9h0cDb+lCbfBf6f7+Lk696KB1ODpePiF5EfGm9qpw=="], "@tursodatabase/database-common": ["@tursodatabase/database-common@0.2.2", "", {}, "sha512-cSNpms6MIaRj29B37XzIu9yGbea0HSGDupZs8QrMxR3rKqgHJIfY04ysqDD/4lS8TNIImPlPZrZgqQz+b/FoKQ=="], @@ -243,44 +117,18 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], - "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], - - "@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], - - "@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="], - - "@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], - - "@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], - - "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], - - "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], - "applesauce-common": ["applesauce-common@6.1.0", "", { "dependencies": { "@scure/base": "^1.2.4", "applesauce-core": "^6.1.0", "hash-sum": "^2.0.0", "light-bolt11-decoder": "^3.2.0", "nanoid": "^5.0.9", "rxjs": "^7.8.1" } }, "sha512-z5azXCIrSknGw57hzUN8F30Xd4clvLCfRQYM0asyyIQJtwxuEl0N/zrXX7RqdiJN/9yFCuLzBro2XoFezoeOMA=="], "applesauce-core": ["applesauce-core@5.2.0", "", { "dependencies": { "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", "nanoid": "^5.0.9", "nostr-tools": "~2.19", "rxjs": "^7.8.1" } }, "sha512-aSuM6q6/Gs2FGUqytlHDjKZpSst2xKaT0vMXUQFWUctECNIxvwy6/hTDDInukMuI9mrQdjnO781ZJJgghI7RNw=="], @@ -291,8 +139,6 @@ "applesauce-wallet-connect": ["applesauce-wallet-connect@6.0.0", "", { "dependencies": { "@noble/hashes": "^1.7.1", "applesauce-common": "^6.0.0", "applesauce-core": "^6.0.0", "rxjs": "^7.8.1" } }, "sha512-rdVvc2NIa6VqnJh2JucyMjhA+S9bohPfXxChJtM4TkWwTFxkhkao9W+7Fuoe9ctelubGKXBbmtOSX3WowjgUOg=="], - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "better-sqlite3": ["better-sqlite3@12.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ=="], @@ -305,16 +151,8 @@ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], - - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], @@ -325,12 +163,6 @@ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - - "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -345,44 +177,36 @@ "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], + "ehbp": ["ehbp@0.2.3", "", { "dependencies": { "@panva/hpke-noble": "^1.0.3", "hpke": "^1.0.1" } }, "sha512-f7iLlhGpeE5xNOYASR3d529PzmBD7Pq5Yb/z0R7vQcxalwNuVTHhX6i4wDJRIVai18MznwoOQ3OWyT/EWaDJ3w=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], - - "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], - "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], - "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], "hash-sum": ["hash-sum@2.0.0", "", {}, "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg=="], + "hpke": ["hpke@1.1.2", "", {}, "sha512-6Z8JoG/zYXly+MfsgQ5aNibBx/HDq2F74254Tv8VK4DR3/Mmf6zVrGakqsodfy9pF0XGKSYh5MA9x6D30gbhbg=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -391,60 +215,24 @@ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], - "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="], "light-bolt11-decoder": ["light-bolt11-decoder@3.2.0", "", { "dependencies": { "@scure/base": "1.1.1" } }, "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ=="], - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], - "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="], "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], @@ -459,12 +247,10 @@ "nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "openai": ["openai@6.44.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"] }, "sha512-09/gH+8jH0RgUwsgWHAaxsKGRT5zVZ95IaJUnqAWj6XejIBmnFRwq2WUIF37VtDEsmGrtPmvCs5+yBSeZGWvkA=="], + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], @@ -473,22 +259,8 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - - "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], - - "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], - "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], - - "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], - "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], @@ -501,18 +273,10 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - - "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], - - "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -521,20 +285,10 @@ "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -543,52 +297,26 @@ "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], - "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], - - "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - - "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], - - "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], - - "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "tinfoil": ["tinfoil@1.1.6", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.41", "@freedomofpress/sigstore-browser": "^0.1.13", "@tinfoilsh/verifier": "1.1.6", "@types/ws": "^8.18.1", "ehbp": "^0.2.0", "openai": "^6.34.0", "ws": "^8.21.0" }, "peerDependencies": { "ai": "^6.0.168" }, "optionalPeers": ["ai"] }, "sha512-kAgVPuvdydLSF72GH8I5Awnc0835MQCNHb9cS/J2TxGgMF2LFgqaJhe9l1iPf7Si64h3s+Gebz0XfTJ80QZ8wQ=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], - "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], - - "vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -601,11 +329,17 @@ "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "zustand": ["zustand@5.0.14", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g=="], - "@routstr/sdk/@cashu/cashu-ts": ["@cashu/cashu-ts@3.7.0", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-Jy4Hek+Wouh94xS4InmTJRLpBPXA6fddVOAHR5lfNyTsCk9r3desY0vnwUGvOZBZWsSKZXedqdD/YpjWb3zngQ=="], + "@freedomofpress/crypto-browser/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - "@routstr/sdk/@types/node": ["@types/node@22.20.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g=="], + "@noble/post-quantum/@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], + + "@panva/hpke-noble/@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], + + "@routstr/sdk/@cashu/cashu-ts": ["@cashu/cashu-ts@3.7.0", "", { "dependencies": { "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0" } }, "sha512-Jy4Hek+Wouh94xS4InmTJRLpBPXA6fddVOAHR5lfNyTsCk9r3desY0vnwUGvOZBZWsSKZXedqdD/YpjWb3zngQ=="], "@scure/bip39/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], @@ -639,13 +373,7 @@ "nostr-tools/@scure/bip32": ["@scure/bip32@2.0.1", "", { "dependencies": { "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA=="], - "postcss/nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="], - - "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "vitest/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - - "@routstr/sdk/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@freedomofpress/crypto-browser/@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "applesauce-common/applesauce-core/nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="], diff --git a/package.json b/package.json index 318036c..02c6114 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "routstrd", - "version": "0.3.8", + "version": "0.3.9", "module": "src/index.ts", "type": "module", "private": false, @@ -24,7 +24,7 @@ }, "dependencies": { "@cashu/cashu-ts": "^4.3.0", - "@routstr/sdk": "^0.3.15", + "@routstr/sdk": "^0.3.16", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", From f90e65aa09f2413171bcd2c66a0a050d798c1a46 Mon Sep 17 00:00:00 2001 From: 9qeklajc <9qeklajc> Date: Sat, 11 Jul 2026 16:33:28 +0200 Subject: [PATCH 38/42] fix breaking hermes config --- bun.lock | 3 + package.json | 1 + src/integrations/hermes.ts | 101 +++++++++++++++++++++--------- tests/integrations/hermes.test.ts | 97 ++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 29 deletions(-) create mode 100644 tests/integrations/hermes.test.ts diff --git a/bun.lock b/bun.lock index 3ef2700..08472a0 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "nostr-tools": "^2.12.0", "qrcode": "^1.5.4", "rxjs": "^7.8.1", + "yaml": "^2.9.0", "zustand": "^5.0.5", }, "devDependencies": { @@ -325,6 +326,8 @@ "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], diff --git a/package.json b/package.json index 02c6114..a45bccc 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "nostr-tools": "^2.12.0", "qrcode": "^1.5.4", "rxjs": "^7.8.1", + "yaml": "^2.9.0", "zustand": "^5.0.5" } } diff --git a/src/integrations/hermes.ts b/src/integrations/hermes.ts index 6fb7bd9..f5f3a28 100644 --- a/src/integrations/hermes.ts +++ b/src/integrations/hermes.ts @@ -1,10 +1,65 @@ import { existsSync, mkdirSync } from "fs"; import { readFile, writeFile } from "fs/promises"; import { dirname } from "path"; +import { parseDocument } from "yaml"; import type { RoutstrdConfig } from "../utils/config"; import type { IntegrationConfig, RoutstrModel } from "./registry"; import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client"; +interface HermesRoutstrConfig { + baseUrl: string; + apiKey: string; + defaultModel: string; +} + +interface HermesCustomProvider { + name?: string; + [key: string]: unknown; +} + +export function mergeHermesConfig( + content: string, + routstr: HermesRoutstrConfig, +): string { + const document = parseDocument(content || "{}", { prettyErrors: true }); + if (document.errors.length > 0) { + throw document.errors[0]; + } + + const urlDisplay = routstr.baseUrl + .replace(/\/v1$/, "") + .replace(/^https?:\/\//, ""); + const provider = { + name: `Routstr (${urlDisplay})`, + base_url: routstr.baseUrl, + api_key: routstr.apiKey, + model: routstr.defaultModel, + }; + + const isNewConfig = content.trim() === ""; + if (isNewConfig) { + document.set("model", { + default: routstr.defaultModel, + provider: "custom", + base_url: routstr.baseUrl, + api_key: routstr.apiKey, + }); + } + + const existingConfig = document.toJS() as { + custom_providers?: HermesCustomProvider[]; + }; + const existingProviders = existingConfig.custom_providers; + const providers = Array.isArray(existingProviders) ? existingProviders : []; + if (providers.some((item) => item.name?.startsWith("Routstr ("))) { + return content; + } + providers.push(provider); + document.set("custom_providers", providers); + + return document.toString(); +} + export async function installHermesIntegration( config: RoutstrdConfig, apiKey: string, @@ -26,10 +81,10 @@ export async function installHermesIntegration( if (models.length >= 3) { defaultModel = models[2]!.id; - console.log(`Set default model to 3rd available model: ${defaultModel}`); + console.log(`Using 3rd available model for new Hermes configurations: ${defaultModel}`); } else if (models.length > 0) { defaultModel = models[0]!.id; - console.log(`Only ${models.length} models available, using ${defaultModel} as default.`); + console.log(`Only ${models.length} models available, using ${defaultModel} for new Hermes configurations.`); } else { console.log("No models available from routstr daemon, using fallback default."); } @@ -47,36 +102,24 @@ export async function installHermesIntegration( console.error(`Error reading ${configPath}, creating new one.`); } - // Remove existing model block - content = content.replace(/^model:\n(?: .*\n)*/gm, ""); - // Remove existing custom_providers block - content = content.replace(/^custom_providers:\n(?:- .*\n(?: .*\n)*)*/gm, ""); - // Clean up extra blank lines - content = content.replace(/\n{3,}/g, "\n\n").trim(); - - const urlDisplay = baseUrl.replace(/^https?:\/\//, ""); - - const modelBlock = `model: - default: ${defaultModel} - provider: custom - base_url: ${baseUrlV1} - api_key: ${apiKey}`; - - const providerBlock = `custom_providers: -- name: Routstr (${urlDisplay}) - base_url: ${baseUrlV1} - api_key: ${apiKey} - model: ${defaultModel}`; - - const parts: string[] = [modelBlock]; - if (content) { - parts.push(content); + let newContent: string; + try { + newContent = mergeHermesConfig(content, { + baseUrl: baseUrlV1, + apiKey, + defaultModel, + }); + } catch (error) { + console.error(`Failed to parse ${configPath} as YAML; leaving it unchanged:`, error); + return; } - parts.push(providerBlock); - - const newContent = parts.join("\n\n") + "\n"; try { + if (newContent === content) { + console.log(`${configPath} already contains current routstr settings.`); + return; + } + mkdirSync(dirname(configPath), { recursive: true }); await writeFile(configPath, newContent); console.log(`Successfully updated ${configPath} with routstr settings.`); diff --git a/tests/integrations/hermes.test.ts b/tests/integrations/hermes.test.ts new file mode 100644 index 0000000..12ac581 --- /dev/null +++ b/tests/integrations/hermes.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from "bun:test"; +import { parse } from "yaml"; +import { mergeHermesConfig } from "../../src/integrations/hermes"; + +const ROUTSTR = { + baseUrl: "http://localhost:8008/v1", + apiKey: "routstr-key", + defaultModel: "deepseek-v4-flash", +}; + +describe("mergeHermesConfig", () => { + it("preserves the user's model and unrelated custom providers", () => { + const existing = `model: + default: user-model + provider: custom + base_url: https://user.example/v1 + api_key: user-key + +custom_providers: + - name: User Provider + base_url: https://user.example/v1 + api_key: user-key + model: user-model + +other_setting: true +`; + + const merged = parse(mergeHermesConfig(existing, ROUTSTR)); + + expect(merged.model).toEqual({ + default: "user-model", + provider: "custom", + base_url: "https://user.example/v1", + api_key: "user-key", + }); + expect(merged.other_setting).toBe(true); + expect(merged.custom_providers).toContainEqual({ + name: "User Provider", + base_url: "https://user.example/v1", + api_key: "user-key", + model: "user-model", + }); + expect(merged.custom_providers).toContainEqual({ + name: "Routstr (localhost:8008)", + base_url: ROUTSTR.baseUrl, + api_key: ROUTSTR.apiKey, + model: ROUTSTR.defaultModel, + }); + }); + + it("does not add a model configuration to an existing Hermes file", () => { + const merged = parse(mergeHermesConfig("other_setting: true\n", ROUTSTR)); + + expect(merged.model).toBeUndefined(); + expect(merged.other_setting).toBe(true); + }); + + it("creates a Routstr default when creating a new Hermes file", () => { + const merged = parse(mergeHermesConfig("", ROUTSTR)); + + expect(merged.model).toEqual({ + default: ROUTSTR.defaultModel, + provider: "custom", + base_url: ROUTSTR.baseUrl, + api_key: ROUTSTR.apiKey, + }); + }); + + it("keeps an existing Routstr provider unchanged without duplicating it", () => { + const existing = `custom_providers: + - name: Routstr (old-host:8008) + base_url: http://old-host:8008/v1 + api_key: old-key + model: old-model +`; + + const once = mergeHermesConfig(existing, ROUTSTR); + const twice = mergeHermesConfig(once, ROUTSTR); + const merged = parse(twice); + const routstrProviders = merged.custom_providers.filter( + (provider: { name?: string }) => provider.name?.startsWith("Routstr ("), + ); + + expect(routstrProviders).toHaveLength(1); + expect(routstrProviders[0]).toEqual({ + name: "Routstr (old-host:8008)", + base_url: "http://old-host:8008/v1", + api_key: "old-key", + model: "old-model", + }); + expect(twice).toBe(once); + }); + + it("rejects malformed YAML instead of replacing it", () => { + expect(() => mergeHermesConfig("model: [unterminated", ROUTSTR)).toThrow(); + }); +}); From 2a31fef10b2cb8d12b17e57bf8d1a0c7be8da76f Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sun, 12 Jul 2026 08:26:44 +0530 Subject: [PATCH 39/42] chore: bump version to 0.3.10 --- bun.lock | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 08472a0..b56af5b 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "name": "routstrd", "dependencies": { "@cashu/cashu-ts": "^4.3.0", - "@routstr/sdk": "^0.3.16", + "@routstr/sdk": "^0.3.17", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", @@ -88,7 +88,7 @@ "@panva/hpke-noble": ["@panva/hpke-noble@1.1.2", "", { "dependencies": { "@noble/ciphers": "^2.2.0", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@noble/post-quantum": "^0.6.1" }, "peerDependencies": { "hpke": "^1.0.0" } }, "sha512-Gr/jKr/ErpRow8aHK+SAp0sAaWy7iJGHlFsXsdTjD8ULcN4vvsVVWR3fiD6Ogf+VoSB28XY/xWRDUEIGXw+9aA=="], - "@routstr/sdk": ["@routstr/sdk@0.3.16", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "ehbp": "^0.2.3", "rxjs": "^7.8.1", "tinfoil": "^1.1.6", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-c5DMRcixvs4fghdYvikBAB+xQ4C9BPg71PRsPn8ZJ+ZxBgr9v5Xm1TXHauB/SpgmowccamXJ5Hd0rB9Ke5MsCw=="], + "@routstr/sdk": ["@routstr/sdk@0.3.17", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "ehbp": "^0.2.3", "rxjs": "^7.8.1", "tinfoil": "^1.1.6", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-3rLsVo8bo3pQG+KzACNfRo3dz6XqfsCoFLpvuqkVNgXUyxSQEQPFRiDtUeACf+a6Xp3f9LSLknVoTeL78s28Tg=="], "@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="], diff --git a/package.json b/package.json index a45bccc..c5b40d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "routstrd", - "version": "0.3.9", + "version": "0.3.10", "module": "src/index.ts", "type": "module", "private": false, @@ -24,7 +24,7 @@ }, "dependencies": { "@cashu/cashu-ts": "^4.3.0", - "@routstr/sdk": "^0.3.16", + "@routstr/sdk": "^0.3.17", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", From 58288114f0b85d1b2f38cf023eae4e1201a604c3 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:39:49 +0800 Subject: [PATCH 40/42] Add Router for Teams section linking to routstrd-auth --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 747bc46..1371447 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ Routstr daemon - A CLI tool for managing routstr processes, similar to `cocod` ( routstrd is a Bun-based CLI tool that provides a background daemon for the Routstr protocol. It integrates with `cocod` for wallet management and uses the Routstr SDK to handle provider routing and model discovery. +## Router for Teams + +For team-based routing, see [routstrd-auth](https://github.com/Routstr/routstrd-auth). + ## Features - **Daemon Mode**: Run routstrd as a background HTTP server From 755dabbc3c29b37d81f2ecfc8015836d7f020598 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:41:13 +0800 Subject: [PATCH 41/42] Fix: Router for Teams -> Routstr for Teams --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1371447..094f30a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Routstr daemon - A CLI tool for managing routstr processes, similar to `cocod` ( routstrd is a Bun-based CLI tool that provides a background daemon for the Routstr protocol. It integrates with `cocod` for wallet management and uses the Routstr SDK to handle provider routing and model discovery. -## Router for Teams +## Routstr for Teams For team-based routing, see [routstrd-auth](https://github.com/Routstr/routstrd-auth). From e07098ae8c788eb6ef10115942e627dbbb381c1c Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:52:34 +0100 Subject: [PATCH 42/42] feat: check for version updates before installing; show update banner in TUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract version-checking logic into src/utils/update-checker.ts (shared between CLI and TUI) - Updating routstrd... bun add v1.2.22 (6bafe260) installed routstrd@0.3.10 with binaries: - routstrd [649.00ms] done routstrd updated successfully. Updating cocod... bun add v1.2.22 (6bafe260) installed @routstr/cocod@0.0.24 with binaries: - cocod [692.00ms] done cocod updated successfully. Both routstrd and cocod have been updated! Using remote daemon — skipping routstrd daemon restart. cocod daemon was not running — skipping restart. ✓ All daemons restarted successfully. now checks npm for the latest version of each package and only reinstalls when a newer version is available; skips daemon restart when nothing was updated - TUI shows a bold yellow 'UPDATE AVAILABLE' banner below the header on all tabs when a new version is detected - TUI checks for updates at most every 210 minutes to avoid spamming the npm registry; first check fires 3s after startup (non-blocking) - Bump version to 0.3.11 --- package.json | 4 +- src/cli.ts | 73 +++++++++++++++-------- src/tui/usage/app.ts | 43 +++++++++++++- src/tui/usage/render.ts | 13 +++- src/tui/usage/types.ts | 6 ++ src/utils/update-checker.ts | 114 ++++++++++++++++++++++++++++++++++++ 6 files changed, 221 insertions(+), 32 deletions(-) create mode 100644 src/utils/update-checker.ts diff --git a/package.json b/package.json index c5b40d1..c85d9a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "routstrd", - "version": "0.3.10", + "version": "0.3.11", "module": "src/index.ts", "type": "module", "private": false, @@ -24,7 +24,7 @@ }, "dependencies": { "@cashu/cashu-ts": "^4.3.0", - "@routstr/sdk": "^0.3.17", + "@routstr/sdk": "^0.3.18", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-wallet-connect": "^6.0.0", diff --git a/src/cli.ts b/src/cli.ts index 06cdd90..477a175 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -36,6 +36,11 @@ import { resolveCocodExecutable, } from "./daemon/wallet/cocod-client"; import packageJson from "../package.json" with { type: "json" }; +import { + compareVersions, + getGlobalPackageVersion, + getLatestNpmVersion, +} from "./utils/update-checker.ts"; type RoutstrModel = { id: string; @@ -363,34 +368,52 @@ program .command("update") .description("Update routstrd and cocod to the latest versions") .action(async () => { - console.log("Updating routstrd..."); - const routstrdProc = Bun.spawn(["bun", "install", "-g", "routstrd"], { - stdout: "inherit", - stderr: "inherit", - }); - const routstrdCode = await routstrdProc.exited; - if (routstrdCode !== 0) { - console.error("Failed to update routstrd."); - process.exit(1); + const packages = [ + { name: "routstrd", label: "routstrd" }, + { name: "@routstr/cocod", label: "cocod" }, + ]; + + let updatedAny = false; + + for (const { name, label } of packages) { + const installed = await getGlobalPackageVersion(name); + const latest = await getLatestNpmVersion(name); + + // Only skip when we're confident the installed version is current. + // If we can't determine either version we fall through to installing. + if ( + installed && + latest && + (compareVersions(installed, latest) ?? -1) >= 0 + ) { + console.log(`${label} is already up to date (v${installed}).`); + continue; + } + + const fromPart = installed ? ` from v${installed}` : ""; + const toPart = latest ? ` to v${latest}` : ""; + console.log(`Updating ${label}${fromPart}${toPart}...`); + + const proc = Bun.spawn(["bun", "install", "-g", name], { + stdout: "inherit", + stderr: "inherit", + }); + const code = await proc.exited; + if (code !== 0) { + console.error(`Failed to update ${label}.`); + process.exit(1); + } + console.log(`${label} updated successfully.\n`); + updatedAny = true; } - console.log("routstrd updated successfully.\n"); - console.log("Updating cocod..."); - const cocodProc = Bun.spawn(["bun", "install", "-g", "@routstr/cocod"], { - stdout: "inherit", - stderr: "inherit", - }); - const cocodCode = await cocodProc.exited; - if (cocodCode !== 0) { - console.error("Failed to update cocod."); - process.exit(1); + if (updatedAny) { + console.log("All requested updates have been applied!"); + // Restart daemons so the new binaries take effect immediately. + await restartDaemonsAfterUpdate(); + } else { + console.log("\nAll packages are already up to date — nothing to do."); } - console.log("cocod updated successfully.\n"); - - console.log("Both routstrd and cocod have been updated!"); - - // Restart daemons so the new binaries take effect immediately. - await restartDaemonsAfterUpdate(); }); program diff --git a/src/tui/usage/app.ts b/src/tui/usage/app.ts index 6eb9424..9f25b67 100644 --- a/src/tui/usage/app.ts +++ b/src/tui/usage/app.ts @@ -28,7 +28,8 @@ import { } from "./terminal.ts"; import { COLORS } from "./constants.ts"; import { renderHeader, renderSearchBar, renderSeparator, renderTabContent, renderTabs } from "./render.ts"; -import type { TabId, UsageStats } from "./types.ts"; +import type { TabId, UpdateInfo, UsageStats } from "./types.ts"; +import { checkForUpdates } from "../../utils/update-checker.ts"; export async function runUsageTui(): Promise { const running = await isDaemonRunning(); @@ -53,6 +54,39 @@ export async function runUsageTui(): Promise { let cleanedUp = false; let fetching = false; + // Update-check state — re-checked at most every 210 minutes to avoid + // spamming the npm registry. The first check happens shortly after + // startup so the TUI doesn't block on a network call. + const UPDATE_CHECK_INTERVAL_MS = 210 * 60 * 1000; + const UPDATE_CHECK_DELAY_MS = 3000; + let updateInfo: UpdateInfo | null = null; + let lastUpdateCheck = 0; + let updateCheckInProgress = false; + + async function maybeCheckForUpdates(): Promise { + if (updateCheckInProgress) return; + const now = Date.now(); + if (updateInfo && now - lastUpdateCheck < UPDATE_CHECK_INTERVAL_MS) return; + + updateCheckInProgress = true; + lastUpdateCheck = now; + try { + const result = await checkForUpdates(); + const outdated = result.packages.filter((p) => p.hasUpdate); + updateInfo = { + hasUpdate: result.hasUpdate, + text: outdated.length > 0 + ? `Update available: ${outdated.map((p) => p.label).join(", ")} ${outdated.map((p) => p.latest).join(", ")}` + : "", + }; + render(); + } catch { + // Silently ignore — update checks are best-effort + } finally { + updateCheckInProgress = false; + } + } + if (isInteractive) { stdout.write(enterAlternateScreen() + hideCursor()); stdin.setRawMode?.(true); @@ -138,7 +172,7 @@ export async function runUsageTui(): Promise { const content = renderTabContent(currentTab, stats, balance, status, width, clients); 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}` : ""}`; - const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar(); + const chrome = renderHeader(currentTab, width, visibleTabs, updateInfo ?? undefined) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar(); const chromeLines = chrome.split("\n").length - 1; const footerSeparator = renderSeparator(width); const footerLines = footerSeparator.split("\n").length - 1; @@ -265,9 +299,14 @@ export async function runUsageTui(): Promise { await fetchData(); + // Kick off the first update check after a short delay so the initial + // data fetch isn't blocked by a network round-trip to npm. + setTimeout(() => void maybeCheckForUpdates(), UPDATE_CHECK_DELAY_MS); + refreshInterval = setInterval(() => { if (autoRefresh) { void fetchData(); } + void maybeCheckForUpdates(); }, 2000); } diff --git a/src/tui/usage/render.ts b/src/tui/usage/render.ts index 727598e..36a0c0a 100644 --- a/src/tui/usage/render.ts +++ b/src/tui/usage/render.ts @@ -8,7 +8,7 @@ import { import { vimState } from "./state.ts"; import { stripAnsi } from "./terminal.ts"; import type { BalanceInfo, StatusInfo } from "./data.ts"; -import type { TabId, UsageStats } from "./types.ts"; +import type { TabId, UpdateInfo, UsageStats } from "./types.ts"; /** Format a cost value: 0.12, 1.23, 12.34, 123.45, 1.23k, 1.23m */ function formatCost(value: number): string { @@ -24,13 +24,20 @@ function formatReqs(value: number): string { return value.toString(); } -export function renderHeader(activeTab: TabId, width: number, visibleTabs: Tab[]): string { +export function renderHeader(activeTab: TabId, width: number, visibleTabs: Tab[], updateInfo?: UpdateInfo): string { const title = `${COLORS.bold}${COLORS.cyan}ROUTSTRD USAGE MONITOR${COLORS.reset}`; const vimIndicator = `${COLORS.yellow}[vim]${COLORS.reset}`; const maxKey = visibleTabs.length; const help = `${COLORS.dim}[Q] Quit [↑↓] Scroll [←→] Tabs [1-${maxKey}] Tabs [R] Refresh${COLORS.reset}`; const fill = width - title.length - help.length - vimIndicator.length - 6; - return `${title}${vimIndicator}${" ".repeat(Math.max(1, fill))}${help}\n`; + const headerLine = `${title}${vimIndicator}${" ".repeat(Math.max(1, fill))}${help}`; + + if (updateInfo?.hasUpdate) { + const banner = `${COLORS.bold}${COLORS.yellow} ↻ UPDATE AVAILABLE — run ${COLORS.green}routstrd update${COLORS.yellow} to update${COLORS.reset}`; + return `${headerLine}\n${banner}\n`; + } + + return `${headerLine}\n`; } export function renderSearchBar(): string { diff --git a/src/tui/usage/types.ts b/src/tui/usage/types.ts index 2c0345d..fd19978 100644 --- a/src/tui/usage/types.ts +++ b/src/tui/usage/types.ts @@ -1,8 +1,14 @@ import type { UsageTrackingEntry } from "../../daemon/types.ts"; import type { UsageSummary } from "../../daemon/http/usage-summary.ts"; +import type { UpdateCheckResult } from "../../utils/update-checker.ts"; export type { UsageSummary }; +export interface UpdateInfo { + hasUpdate: boolean; + text: string; +} + export interface UsageStats { entries: UsageTrackingEntry[]; totalEntries: number; diff --git a/src/utils/update-checker.ts b/src/utils/update-checker.ts new file mode 100644 index 0000000..465415d --- /dev/null +++ b/src/utils/update-checker.ts @@ -0,0 +1,114 @@ +const NPM_REGISTRY = "https://registry.npmjs.org"; + +/** Packages that `routstrd update` manages. */ +export const UPDATE_PACKAGES = [ + { name: "routstrd", label: "routstrd" }, + { name: "@routstr/cocod", label: "cocod" }, +] as const; + +/** + * Fetch the latest published version of a package from the npm registry. + * Returns null if the version cannot be determined (e.g. offline, not found). + */ +export async function getLatestNpmVersion( + packageName: string, +): Promise { + try { + const response = await fetch( + `${NPM_REGISTRY}/${encodeURIComponent(packageName)}/latest`, + ); + if (!response.ok) return null; + const data = (await response.json()) as { version?: string }; + return data.version ?? null; + } catch { + return null; + } +} + +/** + * Get the version of a globally-installed bun package. + * Returns null when the package is not installed globally or the version + * cannot be parsed as semver (e.g. installed from a git URL). + */ +export async function getGlobalPackageVersion( + packageName: string, +): Promise { + try { + const proc = Bun.spawn(["bun", "pm", "ls", "-g"], { + stdout: "pipe", + stderr: "ignore", + }); + const output = await new Response(proc.stdout).text(); + await proc.exited; + // Lines look like: ├── routstrd@0.3.10 or └── @routstr/cocod@0.0.24 + const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = output.match(new RegExp(`${escaped}@([^\\s]+)`)); + if (!match) return null; + const version = match[1]; + // Reject non-semver versions (e.g. github:routstr/cocod#3f6ac14) + if (!version || !/^\d+\.\d+\.\d+/.test(version)) return null; + return version; + } catch { + return null; + } +} + +/** + * Compare two semver version strings. + * Returns a positive number if `a` is newer, negative if `b` is newer, + * 0 if equal, or null if either value is not parseable semver. + */ +export function compareVersions(a: string, b: string): number | null { + const parse = (v: string): [number, number, number] | null => { + const match = v.replace(/^v/, "").match(/^(\d+)\.(\d+)\.(\d+)/); + if (!match?.[1] || !match[2] || !match[3]) return null; + return [ + parseInt(match[1], 10), + parseInt(match[2], 10), + parseInt(match[3], 10), + ]; + }; + const va = parse(a); + const vb = parse(b); + if (!va || !vb) return null; + const [a1, a2, a3] = va; + const [b1, b2, b3] = vb; + if (a1 !== b1) return a1 - b1; + if (a2 !== b2) return a2 - b2; + if (a3 !== b3) return a3 - b3; + return 0; +} + +export interface PackageUpdate { + name: string; + label: string; + current: string | null; + latest: string | null; + hasUpdate: boolean; +} + +export interface UpdateCheckResult { + hasUpdate: boolean; + packages: PackageUpdate[]; +} + +/** + * Check both routstrd and cocod for available updates. + * Returns a result with per-package details and an overall `hasUpdate` flag. + */ +export async function checkForUpdates(): Promise { + const packages = await Promise.all( + UPDATE_PACKAGES.map(async ({ name, label }) => { + const [current, latest] = await Promise.all([ + getGlobalPackageVersion(name), + getLatestNpmVersion(name), + ]); + const hasUpdate = !!(current && latest && (compareVersions(current, latest) ?? -1) < 0); + return { name, label, current, latest, hasUpdate } satisfies PackageUpdate; + }), + ); + return { + hasUpdate: packages.some((p) => p.hasUpdate), + packages, + }; +}