diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57559e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# macOS 本地文件 +.DS_Store + +# Node / Electron 依赖与产物 +node_modules/ +dist/ +release/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# SwiftPM / Xcode 构建缓存 +.build/ +NativeTokenLens/.build/ +DerivedData/ +*.xcuserstate + +# 原生应用打包产物 +NativeTokenLens/TokenLens.app/ +NativeTokenLens/TokenLens.dmg +NativeTokenLens/Assets/AppIcon.iconset/ +native-release/ + +# 本地环境与截图 +.env +.env.* +!.env.example +interface.png +design/*.png diff --git a/.ignore b/.ignore new file mode 100644 index 0000000..57559e8 --- /dev/null +++ b/.ignore @@ -0,0 +1,31 @@ +# macOS 本地文件 +.DS_Store + +# Node / Electron 依赖与产物 +node_modules/ +dist/ +release/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# SwiftPM / Xcode 构建缓存 +.build/ +NativeTokenLens/.build/ +DerivedData/ +*.xcuserstate + +# 原生应用打包产物 +NativeTokenLens/TokenLens.app/ +NativeTokenLens/TokenLens.dmg +NativeTokenLens/Assets/AppIcon.iconset/ +native-release/ + +# 本地环境与截图 +.env +.env.* +!.env.example +interface.png +design/*.png diff --git a/NativeTokenLens/AppBundleInfo.plist b/NativeTokenLens/AppBundleInfo.plist new file mode 100644 index 0000000..5fde6dd --- /dev/null +++ b/NativeTokenLens/AppBundleInfo.plist @@ -0,0 +1,35 @@ + + + + + CFBundleDevelopmentRegion + zh_CN + CFBundleExecutable + TokenLens + CFBundleIdentifier + com.caoxiaozhu.TokenLensNative + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + TokenLens + CFBundleDisplayName + TokenLens + CFBundleIconFile + TokenLensIcon + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.2.0 + CFBundleVersion + 2 + LSApplicationCategoryType + public.app-category.productivity + LSMinimumSystemVersion + 14.0 + NSHighResolutionCapable + + NSSupportsAutomaticGraphicsSwitching + + + diff --git a/NativeTokenLens/Assets/TokenLensIcon.icns b/NativeTokenLens/Assets/TokenLensIcon.icns new file mode 100644 index 0000000..ca183f2 Binary files /dev/null and b/NativeTokenLens/Assets/TokenLensIcon.icns differ diff --git a/NativeTokenLens/Package.swift b/NativeTokenLens/Package.swift new file mode 100644 index 0000000..44a8331 --- /dev/null +++ b/NativeTokenLens/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "NativeTokenLens", + platforms: [ + .macOS(.v14) + ], + products: [ + .library(name: "TokenLensCore", targets: ["TokenLensCore"]), + .executable(name: "TokenLens", targets: ["TokenLens"]) + ], + targets: [ + .target( + name: "TokenLensCore", + linkerSettings: [ + .linkedLibrary("sqlite3") + ] + ), + .executableTarget( + name: "TokenLens", + dependencies: ["TokenLensCore"] + ), + .testTarget( + name: "TokenLensCoreTests", + dependencies: ["TokenLensCore"] + ) + ] +) diff --git a/NativeTokenLens/Sources/TokenLens/AppSettings.swift b/NativeTokenLens/Sources/TokenLens/AppSettings.swift new file mode 100644 index 0000000..b8706eb --- /dev/null +++ b/NativeTokenLens/Sources/TokenLens/AppSettings.swift @@ -0,0 +1,93 @@ +import Combine +import SwiftUI +import Foundation +import AppKit + +enum AutoCollapseDelay: Int, CaseIterable, Identifiable { + case three = 3 + case five = 5 + case ten = 10 + + var id: Int { rawValue } + var label: String { "\(rawValue) 秒" } +} + +enum FloatingAppearance: String, CaseIterable, Identifiable { + case system = "跟随主题" + case light = "亮色" + case dark = "暗色" + + var id: String { rawValue } + + var colorScheme: ColorScheme? { + switch self { + case .system: + return nil + case .light: + return .light + case .dark: + return .dark + } + } + + var nsAppearance: NSAppearance? { + switch self { + case .system: + return nil + case .light: + return NSAppearance(named: .aqua) + case .dark: + return NSAppearance(named: .darkAqua) + } + } +} + +@MainActor +final class AppSettings: ObservableObject { + static let shared = AppSettings() + + @Published var showFloatingWindow: Bool { + didSet { UserDefaults.standard.set(showFloatingWindow, forKey: Self.showFloatingWindowKey) } + } + + @Published var showStatusBarIndicator: Bool { + didSet { UserDefaults.standard.set(showStatusBarIndicator, forKey: Self.showStatusBarIndicatorKey) } + } + + @Published var autoCollapseSeconds: Int { + didSet { UserDefaults.standard.set(autoCollapseSeconds, forKey: Self.autoCollapseSecondsKey) } + } + + @Published var floatingAppearance: FloatingAppearance { + didSet { UserDefaults.standard.set(floatingAppearance.rawValue, forKey: Self.floatingAppearanceKey) } + } + + private static let showFloatingWindowKey = "showFloatingWindow" + private static let showStatusBarIndicatorKey = "showStatusBarIndicator" + private static let autoCollapseSecondsKey = "autoCollapseSeconds" + private static let floatingAppearanceKey = "floatingAppearance" + + private init() { + let defaults = UserDefaults.standard + if defaults.object(forKey: Self.showFloatingWindowKey) == nil { + defaults.set(true, forKey: Self.showFloatingWindowKey) + } + if defaults.object(forKey: Self.showStatusBarIndicatorKey) == nil { + defaults.set(true, forKey: Self.showStatusBarIndicatorKey) + } + if defaults.object(forKey: Self.autoCollapseSecondsKey) == nil { + defaults.set(AutoCollapseDelay.five.rawValue, forKey: Self.autoCollapseSecondsKey) + } + if defaults.object(forKey: Self.floatingAppearanceKey) == nil { + defaults.set(FloatingAppearance.system.rawValue, forKey: Self.floatingAppearanceKey) + } + showFloatingWindow = defaults.bool(forKey: Self.showFloatingWindowKey) + showStatusBarIndicator = defaults.bool(forKey: Self.showStatusBarIndicatorKey) + let collapseSeconds = defaults.integer(forKey: Self.autoCollapseSecondsKey) + autoCollapseSeconds = AutoCollapseDelay.allCases.map(\.rawValue).contains(collapseSeconds) + ? collapseSeconds + : AutoCollapseDelay.five.rawValue + let appearanceRawValue = defaults.string(forKey: Self.floatingAppearanceKey) ?? FloatingAppearance.system.rawValue + floatingAppearance = FloatingAppearance(rawValue: appearanceRawValue) ?? .system + } +} diff --git a/NativeTokenLens/Sources/TokenLens/BrandMark.swift b/NativeTokenLens/Sources/TokenLens/BrandMark.swift new file mode 100644 index 0000000..fa0b6a7 --- /dev/null +++ b/NativeTokenLens/Sources/TokenLens/BrandMark.swift @@ -0,0 +1,79 @@ +import SwiftUI + +struct BrandMark: View { + let phase: Double + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 13, style: .continuous) + .fill( + LinearGradient( + colors: [ + Color(red: 0.09, green: 0.30, blue: 0.82), + Color(red: 0.44, green: 0.20, blue: 0.94), + Color(red: 0.11, green: 0.71, blue: 0.94) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .overlay { + RoundedRectangle(cornerRadius: 13, style: .continuous) + .stroke(.white.opacity(0.28), lineWidth: 1) + } + .shadow(color: .blue.opacity(0.28), radius: 14, y: 8) + + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(.white.opacity(0.18)) + .frame(width: 34, height: 16) + .blur(radius: 10) + .offset(x: -8, y: -12) + + HStack(alignment: .bottom, spacing: 4) { + Capsule() + .frame(width: 5, height: barHeight(base: 18, amplitude: 5, offset: 0)) + Capsule() + .frame(width: 5, height: barHeight(base: 27, amplitude: 7, offset: 0.8)) + Capsule() + .frame(width: 5, height: barHeight(base: 14, amplitude: 4, offset: 1.6)) + } + .foregroundStyle(.white.opacity(0.94)) + .offset(x: -6, y: 4) + + Circle() + .stroke(.white.opacity(0.92), lineWidth: 3.2) + .frame(width: 17, height: 17) + .offset(x: 12, y: 6) + Capsule() + .fill(.white.opacity(0.92)) + .frame(width: 13, height: 3.2) + .rotationEffect(.degrees(-45)) + .offset(x: 21, y: 15) + } + .rotation3DEffect(.degrees(sin(phase) * 2.4), axis: (x: 0, y: 1, z: 0)) + .scaleEffect(1 + sin(phase * 0.7) * 0.018) + } + + private func barHeight(base: Double, amplitude: Double, offset: Double) -> CGFloat { + CGFloat(base + sin(phase + offset) * amplitude) + } +} + +struct AnimatedAppearance: ViewModifier { + let appeared: Bool + let delay: Double + + func body(content: Content) -> some View { + content + .opacity(appeared ? 1 : 0) + .offset(y: appeared ? 0 : 18) + .scaleEffect(appeared ? 1 : 0.985) + .animation(.smooth(duration: 0.52).delay(delay), value: appeared) + } +} + +extension View { + func animatedAppearance(_ appeared: Bool, delay: Double) -> some View { + modifier(AnimatedAppearance(appeared: appeared, delay: delay)) + } +} diff --git a/NativeTokenLens/Sources/TokenLens/ChartsAndTables.swift b/NativeTokenLens/Sources/TokenLens/ChartsAndTables.swift new file mode 100644 index 0000000..61449d7 --- /dev/null +++ b/NativeTokenLens/Sources/TokenLens/ChartsAndTables.swift @@ -0,0 +1,816 @@ +import Charts +import SwiftUI +import TokenLensCore + +struct TrendChart: View { + let summary: UsageSummary + let mode: TrendMode + + private var points: [DailyPoint] { + summary.trendPoints(mode: mode) + } + + private var chartPoints: [ProviderTrendPoint] { + points.flatMap { point in + Provider.allCases.map { provider in + ProviderTrendPoint( + mode: mode, + date: point.date, + provider: provider, + tokens: point.values[provider, default: 0] + ) + } + } + } + + private var maxValue: Int { + max(1, chartPoints.map(\.tokens).max() ?? 1) + } + + var body: some View { + Chart(chartPoints) { point in + LineMark( + x: .value("日期", point.date), + y: .value("Tokens", point.tokens), + series: .value("工具", point.provider.rawValue) + ) + .interpolationMethod(.linear) + .lineStyle(StrokeStyle(lineWidth: 3.5, lineCap: .round, lineJoin: .round)) + .foregroundStyle(by: .value("工具", point.provider.rawValue)) + + AreaMark( + x: .value("日期", point.date), + y: .value("Tokens", point.tokens), + series: .value("工具", point.provider.rawValue) + ) + .interpolationMethod(.linear) + .foregroundStyle(by: .value("工具", point.provider.rawValue)) + .opacity(point.provider == .codex ? 0.10 : 0.03) + } + .chartForegroundStyleScale(providerStyleScale) + .chartLegend(position: .bottom, alignment: .center, spacing: 18) + .chartYScale(domain: 0...(Double(maxValue) * 1.08)) + .chartPlotStyle { plot in + plot.clipped() + } + .chartYAxis { + AxisMarks(position: .leading, values: .automatic(desiredCount: 5)) { value in + AxisGridLine(stroke: StrokeStyle(lineWidth: 1)) + .foregroundStyle(.white.opacity(0.08)) + AxisValueLabel { + if let tokens = value.as(Double.self) { + Text(TokenFormatter.format(Int(tokens))) + .foregroundStyle(.secondary) + } + } + } + } + .chartXAxis { + AxisMarks(values: .automatic(desiredCount: mode == .daily ? 6 : 5)) { value in + AxisGridLine() + .foregroundStyle(.clear) + AxisValueLabel { + if let date = value.as(Date.self) { + Text(axisLabel(for: date)) + .foregroundStyle(.secondary) + } + } + } + } + .frame(height: 235) + .animation(.spring(response: 0.42, dampingFraction: 0.88), value: mode) + } + + private func axisLabel(for date: Date) -> String { + switch mode { + case .daily: + return date.formatted(.dateTime.hour(.twoDigits(amPM: .omitted))) + case .weekly: + return date.formatted(.dateTime.month(.twoDigits).day(.twoDigits)) + case .monthly: + return date.formatted(.dateTime.month(.twoDigits).day(.twoDigits)) + case .yearly: + return date.formatted(.dateTime.year(.twoDigits).month(.twoDigits)) + } + } +} + +struct UsageShare: View { + let summary: UsageSummary + + var total: Int { + Provider.allCases.reduce(0) { $0 + (summary.providerTotals[$1]?.totalTokens ?? 0) } + } + + private var visibleProviders: [Provider] { + let active = Provider.allCases.filter { + (summary.providerTotals[$0]?.totalTokens ?? 0) > 0 + } + return active.isEmpty ? Provider.allCases : active + } + + var body: some View { + HStack(spacing: 28) { + VStack(spacing: 10) { + PieChartView(summary: summary) + .frame(width: 190, height: 190) + HStack(spacing: 8) { + Text("本月") + .foregroundStyle(.secondary) + Text(TokenFormatter.format(total)) + .fontWeight(.bold) + .monospacedDigit() + } + .font(.system(size: 14, weight: .semibold, design: .rounded)) + } + VStack(spacing: 12) { + ForEach(visibleProviders, id: \.self) { provider in + let providerTotal = summary.providerTotals[provider]?.totalTokens ?? 0 + let percent = total > 0 ? Int((Double(providerTotal) / Double(total) * 100).rounded()) : 0 + HStack { + Circle().fill(provider.color).frame(width: 10, height: 10) + VStack(alignment: .leading, spacing: 2) { + Text(provider.rawValue) + .font(.system(size: 15, weight: .semibold)) + Text("\(TokenFormatter.format(providerTotal)) Tokens") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(.secondary) + } + Spacer() + Text("\(percent)%") + .font(.system(size: 19, weight: .bold, design: .rounded)) + } + } + } + } + .frame(height: 235) + } +} + +struct PieChartView: View { + let summary: UsageSummary + + var total: Int { + Provider.allCases.reduce(0) { $0 + (summary.providerTotals[$1]?.totalTokens ?? 0) } + } + + private var slices: [ProviderSlice] { + Provider.allCases.map { provider in + ProviderSlice( + provider: provider, + tokens: summary.providerTotals[provider]?.totalTokens ?? 0 + ) + } + } + + var body: some View { + Chart(slices) { slice in + SectorMark( + angle: .value("Tokens", max(slice.tokens, 0)), + angularInset: 1.8 + ) + .cornerRadius(6) + .foregroundStyle(by: .value("工具", slice.provider.rawValue)) + .opacity(total > 0 && slice.tokens > 0 ? 1 : 0) + } + .chartForegroundStyleScale(providerStyleScale) + .chartLegend(.hidden) + .chartBackground { _ in + if total == 0 { + Circle() + .fill(.white.opacity(0.10)) + } + } + .shadow(color: .purple.opacity(0.24), radius: 18, y: 12) + } +} + +struct ToolBreakdown: View { + let rows: [ToolRow] + @Binding var selection: String + private let visibleRowLimit = ToolBreakdownLayout.visibleRowLimit + + private var visibleRowCount: Int { + min(max(rows.count, 1), visibleRowLimit) + } + + private var rowViewportHeight: CGFloat { + CGFloat(visibleRowCount) * ToolBreakdownLayout.rowHeight + } + + private var providerSignature: String { + rows.map { $0.provider.rawValue }.joined(separator: "|") + } + + var body: some View { + GlassCard { + VStack(spacing: 0) { + HStack { + Text("工具明细") + .font(.system(size: 21, weight: .bold, design: .rounded)) + Spacer() + NativeMenuPicker(selection: $selection, options: ["全部工具"] + Provider.allCases.map(\.rawValue)) + .frame(width: 142) + } + .padding(.bottom, 12) + + VStack(spacing: 0) { + ToolBreakdownHeader() + ScrollView(showsIndicators: rows.count > visibleRowLimit) { + LazyVStack(spacing: 0) { + if rows.isEmpty { + Text("暂无工具数据") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity) + .frame(height: ToolBreakdownLayout.rowHeight) + } else { + ForEach(rows) { row in + ToolBreakdownRow(row: row) + if row.id != rows.last?.id { + Divider() + .overlay(.white.opacity(0.08)) + } + } + } + } + } + .id(providerSignature) + .frame(height: rowViewportHeight) + } + .padding(.horizontal, 12) + .padding(.vertical, 5) + .background(.white.opacity(0.035), in: RoundedRectangle(cornerRadius: 18, style: .continuous)) + .overlay(RoundedRectangle(cornerRadius: 18, style: .continuous).stroke(.white.opacity(0.08))) + } + .frame(maxHeight: .infinity, alignment: .top) + } + } +} + +private enum ToolBreakdownLayout { + static let visibleRowLimit = 3 + static let rowHeight: CGFloat = 54 + static let headerHeight: CGFloat = 38 +} + +private enum ToolBreakdownColumns { + static let spacing: CGFloat = 16 + + static func widths(totalWidth: CGFloat) -> ToolBreakdownColumnWidths { + let contentWidth = max(0, totalWidth - spacing * 5) + return ToolBreakdownColumnWidths( + tool: contentWidth * 0.24, + metric: contentWidth * 0.15, + trend: contentWidth * 0.16 + ) + } +} + +private struct ToolBreakdownColumnWidths { + let tool: CGFloat + let metric: CGFloat + let trend: CGFloat +} + +private struct ToolBreakdownHeader: View { + var body: some View { + GeometryReader { proxy in + let widths = ToolBreakdownColumns.widths(totalWidth: proxy.size.width) + HStack(spacing: ToolBreakdownColumns.spacing) { + header("工具名称", width: widths.tool, alignment: .leading) + header("输入", width: widths.metric) + header("输出", width: widths.metric) + header("缓存", width: widths.metric) + header("总量", width: widths.metric) + header("趋势", width: widths.trend) + } + .frame(width: proxy.size.width, height: proxy.size.height) + } + .frame(maxWidth: .infinity) + .padding(.horizontal, 14) + .frame(height: ToolBreakdownLayout.headerHeight) + .overlay(alignment: .bottom) { + Rectangle() + .fill(.white.opacity(0.10)) + .frame(height: 1) + } + } + + private func header(_ text: String, width: CGFloat, alignment: Alignment = .center) -> some View { + Text(text) + .font(.system(size: 13, weight: .bold)) + .foregroundStyle(.secondary) + .frame(width: width, alignment: alignment) + } +} + +private struct ToolBreakdownRow: View { + let row: ToolRow + + var body: some View { + GeometryReader { proxy in + let widths = ToolBreakdownColumns.widths(totalWidth: proxy.size.width) + HStack(spacing: ToolBreakdownColumns.spacing) { + providerCell(row.provider) + .frame(width: widths.tool, alignment: .leading) + metric(TokenFormatter.format(row.inputTokens), width: widths.metric) + metric(TokenFormatter.format(row.outputTokens), width: widths.metric) + metric(TokenFormatter.format(row.cachedTokens), width: widths.metric) + metric(TokenFormatter.format(row.totalTokens), width: widths.metric) + Sparkline(provider: row.provider, values: row.trendTokens) + .frame(width: 112, height: 30) + .frame(width: widths.trend, alignment: .center) + } + .frame(width: proxy.size.width, height: proxy.size.height) + } + .frame(maxWidth: .infinity) + .padding(.horizontal, 14) + .frame(height: ToolBreakdownLayout.rowHeight) + } + + private func metric(_ text: String, width: CGFloat) -> some View { + Text(text) + .font(.system(size: 16, weight: .bold, design: .rounded)) + .monospacedDigit() + .lineLimit(1) + .minimumScaleFactor(0.68) + .frame(width: width, alignment: .center) + } + + private func providerBadge(_ provider: Provider) -> some View { + RoundedRectangle(cornerRadius: 9) + .fill(provider.color.gradient) + .frame(width: 30, height: 30) + .overlay { + Text(String(provider.rawValue.prefix(1))) + .font(.system(size: 13, weight: .bold)) + } + } + + private func providerCell(_ provider: Provider) -> some View { + HStack(spacing: 12) { + providerBadge(provider) + Text(provider.rawValue) + .font(.system(size: 15, weight: .semibold)) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +struct Sparkline: View { + let provider: Provider + let values: [Int] + + var body: some View { + Chart(points) { point in + LineMark( + x: .value("序号", point.index), + y: .value("趋势", point.value) + ) + .interpolationMethod(.linear) + .lineStyle(StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round)) + .foregroundStyle(provider.color) + } + .chartXAxis(.hidden) + .chartYAxis(.hidden) + .chartLegend(.hidden) + .chartPlotStyle { plot in + plot.background(.clear) + } + .chartYScale(domain: yDomain) + } + + private var points: [SparkPoint] { + let source = values.isEmpty ? [0, 0] : values + let padded = source.count == 1 ? [0, source[0]] : source + return padded.enumerated().map { + SparkPoint(index: $0.offset, value: $0.element) + } + } + + private var yDomain: ClosedRange { + let maxValue = max(1, points.map(\.value).max() ?? 1) + return 0...maxValue + } +} + +private struct ProviderTrendPoint: Identifiable, Hashable { + var id: String { + "\(mode.rawValue)-\(Int(date.timeIntervalSince1970))-\(provider.rawValue)" + } + + let mode: TrendMode + let date: Date + let provider: Provider + let tokens: Int +} + +private struct ProviderSlice: Identifiable, Hashable { + let id = UUID() + let provider: Provider + let tokens: Int +} + +private struct SparkPoint: Identifiable, Hashable { + let id = UUID() + let index: Int + let value: Int +} + +private var providerStyleScale: KeyValuePairs { + [ + Provider.claude.rawValue: Provider.claude.color, + Provider.codex.rawValue: Provider.codex.color, + Provider.gemini.rawValue: Provider.gemini.color, + Provider.hermes.rawValue: Provider.hermes.color, + Provider.opencode.rawValue: Provider.opencode.color + ] +} + +struct TokenActivityHeatmap: View { + let summary: UsageSummary + @State private var mode = "每日" + @State private var hoveredDay: HeatmapDay? + @State private var tooltipPosition: CGPoint? + + private let cellWidth: CGFloat = 10 + private let cellHeight: CGFloat = 18 + private let cellSpacing: CGFloat = 3 + private let modes = ["每日", "每周", "累计"] + + private var weeks: [[HeatmapDay]] { + makeHeatmapWeeks(mode: mode) + } + + private var maxValue: Int { + max(1, weeks.flatMap { $0 }.map(\.value).max() ?? 1) + } + + private var selectedDay: HeatmapDay? { + hoveredDay ?? weeks.flatMap { $0 }.last { !$0.isFuture } + } + + private var visibleDays: [HeatmapDay] { + weeks.flatMap { $0 }.filter { !$0.isFuture } + } + + private var activeDays: Int { + visibleDays.filter { $0.dailyTotal > 0 }.count + } + + private var averageTokens: Int { + guard !visibleDays.isEmpty else { return 0 } + return visibleDays.reduce(0) { $0 + $1.dailyTotal } / visibleDays.count + } + + private var peakTokens: Int { + visibleDays.map(\.dailyTotal).max() ?? 0 + } + + var body: some View { + GlassCard { + VStack(alignment: .leading, spacing: 12) { + HStack(alignment: .firstTextBaseline) { + VStack(alignment: .leading, spacing: 3) { + Text("Token 活动") + .font(.system(size: 21, weight: .bold, design: .rounded)) + if let selectedDay { + Text("\(selectedDay.date.formatted(.dateTime.month().day())) · \(TokenFormatter.format(selectedDay.dailyTotal)) Tokens") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.secondary) + .monospacedDigit() + } + } + Spacer() + HStack(spacing: 12) { + ForEach(modes, id: \.self) { item in + Button(item) { + withAnimation(.smooth(duration: 0.22)) { + mode = item + } + } + .buttonStyle(.plain) + .font(.system(size: 13, weight: mode == item ? .bold : .semibold)) + .foregroundStyle(mode == item ? .primary : .secondary) + } + } + } + + heatmapGrid + .frame(maxWidth: .infinity, alignment: .center) + .animation(.smooth(duration: 0.25), value: weeks) + + HStack { + Text(monthRangeText) + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(.secondary) + Spacer() + HeatmapLegend { value in + color(for: HeatmapDay(date: summary.generatedAt, value: value, dailyTotal: value, isFuture: false)) + } + } + + HStack(spacing: 8) { + HeatmapStat(title: "活跃", value: "\(activeDays) 天") + HeatmapStat(title: "日均", value: TokenFormatter.format(averageTokens)) + HeatmapStat(title: "峰值", value: TokenFormatter.format(peakTokens)) + } + Spacer(minLength: 0) + } + .frame(maxHeight: .infinity, alignment: .top) + } + } + + private var heatmapGrid: some View { + let gridWidth = CGFloat(weeks.count) * cellWidth + CGFloat(max(weeks.count - 1, 0)) * cellSpacing + let gridHeight = CGFloat(7) * cellHeight + CGFloat(6) * cellSpacing + + return ZStack(alignment: .topLeading) { + HStack(alignment: .top, spacing: cellSpacing) { + ForEach(Array(weeks.enumerated()), id: \.offset) { weekIndex, week in + VStack(spacing: cellSpacing) { + ForEach(Array(week.enumerated()), id: \.element.id) { dayIndex, day in + RoundedRectangle(cornerRadius: 5, style: .continuous) + .fill(color(for: day)) + .frame(width: cellWidth, height: cellHeight) + .overlay { + RoundedRectangle(cornerRadius: 5, style: .continuous) + .stroke(.white.opacity(day.dailyTotal > 0 ? 0.18 : 0.03), lineWidth: hoveredDay?.id == day.id ? 1.2 : 0.6) + } + .opacity(day.isFuture ? 0 : 1) + .scaleEffect(hoveredDay?.id == day.id ? 1.16 : 1) + .onHover { isInside in + if isInside && !day.isFuture { + hoveredDay = day + tooltipPosition = CGPoint( + x: CGFloat(weekIndex) * (cellWidth + cellSpacing) + cellWidth / 2, + y: CGFloat(dayIndex) * (cellHeight + cellSpacing) + ) + } else if hoveredDay?.id == day.id { + hoveredDay = nil + tooltipPosition = nil + } + } + } + } + } + } + .frame(width: gridWidth, height: gridHeight, alignment: .topLeading) + + if let hoveredDay, let tooltipPosition { + HeatmapTooltip(day: hoveredDay, mode: mode) + .fixedSize() + .position( + x: min(max(tooltipPosition.x, 94), gridWidth - 94), + y: max(-20, tooltipPosition.y - 18) + ) + .transition(.scale(scale: 0.96).combined(with: .opacity)) + .zIndex(10) + } + } + .frame(width: gridWidth, height: gridHeight, alignment: .topLeading) + .animation(.smooth(duration: 0.16), value: hoveredDay) + } + + private var monthRangeText: String { + let flat = weeks.flatMap { $0 }.filter { !$0.isFuture } + guard let first = flat.first?.date, let last = flat.last?.date else { return "最近活动" } + return "\(first.formatted(.dateTime.month())) - \(last.formatted(.dateTime.month()))" + } + + private func color(for day: HeatmapDay) -> Color { + guard day.value > 0 else { return .white.opacity(0.06) } + let ratio = min(1, sqrt(Double(day.value) / Double(maxValue))) + return Color.cyan.opacity(0.18 + ratio * 0.72) + } + + private func makeHeatmapWeeks(mode: String) -> [[HeatmapDay]] { + var calendar = Calendar.current + calendar.firstWeekday = 2 + let end = calendar.startOfDay(for: summary.generatedAt) + let rawStart = calendar.date(byAdding: .day, value: -167, to: end) ?? end + let weekday = calendar.component(.weekday, from: rawStart) + let daysFromMonday = (weekday + 5) % 7 + let start = calendar.date(byAdding: .day, value: -daysFromMonday, to: rawStart) ?? rawStart + let details = Dictionary(uniqueKeysWithValues: summary.dailyUsageDetails(calendar: calendar).map { ($0.date, $0) }) + + var days: [HeatmapDay] = [] + var cumulative = 0 + var cursor = start + while cursor <= (calendar.date(byAdding: .day, value: 6, to: end) ?? end) { + let detail = details[calendar.startOfDay(for: cursor)] + let dailyTotal = detail?.totalTokens ?? 0 + cumulative += cursor <= end ? dailyTotal : 0 + let value: Int + switch mode { + case "每周": + value = weeklyTotal(for: cursor, details: details, calendar: calendar) + case "累计": + value = cumulative + default: + value = dailyTotal + } + days.append(HeatmapDay(date: cursor, value: value, dailyTotal: dailyTotal, isFuture: cursor > end)) + guard let next = calendar.date(byAdding: .day, value: 1, to: cursor) else { break } + cursor = next + } + + return stride(from: 0, to: days.count, by: 7).map { startIndex in + Array(days[startIndex.. Int { + guard let interval = calendar.dateInterval(of: .weekOfYear, for: date) else { return 0 } + return details.values.reduce(0) { total, detail in + interval.contains(detail.date) ? total + detail.totalTokens : total + } + } +} + +private struct HeatmapDay: Identifiable, Hashable { + var id: Date { date } + let date: Date + let value: Int + let dailyTotal: Int + let isFuture: Bool +} + +private struct HeatmapTooltip: View { + let day: HeatmapDay + let mode: String + + var body: some View { + HStack(spacing: 8) { + Text(day.date.formatted(.dateTime.month().day())) + .fontWeight(.bold) + Text(tooltipText) + } + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .foregroundStyle(.white) + .monospacedDigit() + .padding(.horizontal, 14) + .frame(height: 38) + .background { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color(red: 0.13, green: 0.13, blue: 0.14).opacity(0.96)) + .overlay { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(.white.opacity(0.16), lineWidth: 1) + } + .shadow(color: .black.opacity(0.32), radius: 16, y: 10) + } + } + + private var tooltipText: String { + switch mode { + case "每周": + return "所在周使用了 \(TokenFormatter.format(day.value)) 个 Token" + case "累计": + return "累计使用了 \(TokenFormatter.format(day.value)) 个 Token" + default: + return "使用了 \(TokenFormatter.format(day.dailyTotal)) 个 Token" + } + } +} + +private struct HeatmapLegend: View { + let color: (Int) -> Color + + var body: some View { + HStack(spacing: 5) { + Text("少") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(.secondary) + ForEach([0, 1, 2, 3], id: \.self) { index in + RoundedRectangle(cornerRadius: 3, style: .continuous) + .fill(color(index + 1)) + .frame(width: 10, height: 10) + } + Text("多") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(.secondary) + } + } +} + +private struct HeatmapStat: View { + let title: String + let value: String + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(.secondary) + Text(value) + .font(.system(size: 13, weight: .bold, design: .rounded)) + .monospacedDigit() + } + .padding(.horizontal, 10) + .frame(maxWidth: .infinity, minHeight: 38, alignment: .leading) + .background(.white.opacity(0.06), in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .overlay(RoundedRectangle(cornerRadius: 12).stroke(.white.opacity(0.08))) + } +} + +struct InsightsPanel: View { + let insights: [Insight] + + var body: some View { + GlassCard { + VStack(spacing: 12) { + HStack { + Text("洞察提醒") + .font(.system(size: 21, weight: .bold, design: .rounded)) + Spacer() + Text("查看全部") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(.blue) + } + ForEach(insights) { insight in + HStack(alignment: .top, spacing: 12) { + RoundedRectangle(cornerRadius: 10) + .fill(insight.tone.color.opacity(0.2)) + .frame(width: 36, height: 36) + .overlay { + Image(systemName: insight.tone.symbol) + .font(.system(size: 17, weight: .bold)) + .foregroundStyle(insight.tone.color) + } + VStack(alignment: .leading, spacing: 4) { + Text(insight.title) + .font(.system(size: 15, weight: .bold)) + Text(insight.message) + .font(.system(size: 13)) + .foregroundStyle(.secondary) + } + Spacer() + } + .padding(12) + .background(insight.tone.color.opacity(0.08), in: RoundedRectangle(cornerRadius: 16)) + .overlay(RoundedRectangle(cornerRadius: 16).stroke(insight.tone.color.opacity(0.2))) + } + } + } + } +} + +struct AmbientBackground: View { + var body: some View { + ZStack { + LinearGradient(colors: [Color(red: 0.03, green: 0.06, blue: 0.11), Color(red: 0.04, green: 0.09, blue: 0.15)], startPoint: .topLeading, endPoint: .bottomTrailing) + Circle() + .fill(.blue.opacity(0.16)) + .frame(width: 520, height: 520) + .blur(radius: 90) + .offset(x: -380, y: -260) + Circle() + .fill(.cyan.opacity(0.12)) + .frame(width: 430, height: 430) + .blur(radius: 80) + .offset(x: 420, y: -210) + Circle() + .fill(.purple.opacity(0.14)) + .frame(width: 520, height: 520) + .blur(radius: 110) + .offset(x: 280, y: 350) + } + .ignoresSafeArea() + } +} + +extension Provider { + var color: Color { + switch self { + case .claude: .blue + case .codex: .purple + case .gemini: .cyan + case .hermes: .green + case .opencode: .orange + } + } +} + +extension Insight.Tone { + var color: Color { + switch self { + case .warning: .orange + case .positive: .green + case .accent: .purple + } + } + + var symbol: String { + switch self { + case .warning: "exclamationmark" + case .positive: "checkmark" + case .accent: "arrow.up.right" + } + } +} diff --git a/NativeTokenLens/Sources/TokenLens/DashboardView.swift b/NativeTokenLens/Sources/TokenLens/DashboardView.swift new file mode 100644 index 0000000..e938e46 --- /dev/null +++ b/NativeTokenLens/Sources/TokenLens/DashboardView.swift @@ -0,0 +1,866 @@ +import AppKit +import SwiftUI +import TokenLensCore + +struct DashboardView: View { + @StateObject private var store = UsageStore.shared + @State private var appeared = false + @State private var showingDetails = false + @State private var trendMode = "每日" + @State private var shareMode = "按 Tokens" + @State private var toolFilter = "全部工具" + + var filteredRows: [ToolRow] { + if toolFilter == "全部工具" { return store.summary.toolRows } + return store.summary.toolRows.filter { $0.provider.rawValue == toolFilter } + } + + private var bottomPanelHeight: CGFloat { + 336 + } + + private var sidePanelWidth: CGFloat { + 420 + } + + var body: some View { + ZStack { + AmbientBackground() + if showingDetails { + DailyUsageDetailPage(summary: store.summary) { + withAnimation(pageAnimation) { + showingDetails = false + } + } + .transition(.asymmetric( + insertion: .move(edge: .trailing).combined(with: .opacity), + removal: .move(edge: .trailing).combined(with: .opacity) + )) + } else { + dashboardContent + .transition(.asymmetric( + insertion: .move(edge: .leading).combined(with: .opacity), + removal: .move(edge: .leading).combined(with: .opacity) + )) + } + } + .animation(pageAnimation, value: showingDetails) + .overlay(alignment: .top) { + TitlebarDoubleClickZone() + .frame(height: 32) + .ignoresSafeArea(edges: .top) + } + .task { + appeared = true + store.start() + } + } + + private var pageAnimation: Animation { + .spring(response: 0.42, dampingFraction: 0.90, blendDuration: 0.08) + } + + private var dashboardContent: some View { + VStack(spacing: 0) { + HeaderBar(store: store) { + withAnimation(pageAnimation) { + showingDetails = true + } + } + .animatedAppearance(appeared, delay: 0.02) + VStack(spacing: 0) { + ScrollView(showsIndicators: false) { + VStack(spacing: 18) { + MetricGrid(cards: store.summary.cards) + .animatedAppearance(appeared, delay: 0.08) + HStack(spacing: 18) { + GlassPanel(title: "Tokens 使用趋势") { + TrendChart(summary: store.summary, mode: TrendMode(rawValue: trendMode) ?? .daily) + } accessory: { + NativeMenuPicker(selection: $trendMode, options: TrendMode.allCases.map(\.rawValue)) + .frame(width: 126) + } + .frame(maxWidth: .infinity) + + GlassPanel(title: "使用占比") { + UsageShare(summary: store.summary) + } accessory: { + NativeMenuPicker(selection: $shareMode, options: ["按 Tokens", "按占比"]) + .frame(width: 154) + } + .frame(width: sidePanelWidth) + } + .animatedAppearance(appeared, delay: 0.15) + HStack(alignment: .top, spacing: 18) { + ToolBreakdown(rows: filteredRows, selection: $toolFilter) + .frame(maxWidth: .infinity) + .frame(height: bottomPanelHeight, alignment: .top) + TokenActivityHeatmap(summary: store.summary) + .frame(width: sidePanelWidth) + .frame(height: bottomPanelHeight, alignment: .top) + } + .frame(height: bottomPanelHeight) + .animatedAppearance(appeared, delay: 0.22) + } + .padding(.horizontal, 26) + .padding(.top, 22) + .padding(.bottom, 26) + } + } + } + } +} + +private struct TitlebarDoubleClickZone: NSViewRepresentable { + func makeNSView(context: Context) -> NSView { + TitlebarHitView() + } + + func updateNSView(_ nsView: NSView, context: Context) {} +} + +private final class TitlebarHitView: NSView { + override var acceptsFirstResponder: Bool { true } + + override func mouseDown(with event: NSEvent) { + guard event.clickCount >= 2 else { + window?.performDrag(with: event) + return + } + if let window { + WindowActions.toggleMaximize(window) + } + } +} + +struct HeaderBar: View { + @ObservedObject var store: UsageStore + let onShowDetails: () -> Void + + var body: some View { + HStack(spacing: 16) { + BrandMark(phase: 0) + .frame(width: 42, height: 42) + Text("TokenLens 用量统计") + .font(.system(size: 24, weight: .bold, design: .rounded)) + + StatusPill(isRefreshing: store.isRefreshing, phase: 0) + + Spacer() + + Button(action: onShowDetails) { + Label("查看详情", systemImage: "list.bullet.rectangle.portrait") + .labelStyle(.titleAndIcon) + } + .buttonStyle(GlassButtonStyle()) + + VStack(alignment: .leading, spacing: 2) { + Text("上次更新 \(store.summary.generatedAt.formatted(date: .omitted, time: .shortened))") + Text("已刷新 \(store.refreshCount) 次") + .foregroundStyle(.secondary) + } + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(.secondary) + } + .padding(.leading, 26) + .padding(.trailing, 28) + .frame(height: 78) + .background(.ultraThinMaterial) + .overlay(alignment: .bottom) { + Rectangle() + .fill(.white.opacity(0.08)) + .frame(height: 1) + } + } +} + +struct DailyUsageDetailPage: View { + let summary: UsageSummary + let onBack: () -> Void + @State private var selectedYear = "" + @State private var selectedMonth = "" + @State private var selectedDay = "全部" + + private var details: [DailyUsageDetail] { + summary.dailyUsageDetails() + } + + private var calendar: Calendar { + var calendar = Calendar.current + calendar.firstWeekday = 2 + return calendar + } + + private var yearOptions: [String] { + let years = Set(details.map { calendar.component(.year, from: $0.date) }) + let fallbackYear = calendar.component(.year, from: summary.generatedAt) + let sortedYears = years.isEmpty ? [fallbackYear] : years.sorted(by: >) + return sortedYears.map { "\($0)年" } + } + + private var monthLabels: [String] { + (1...12).map { "\($0)月" } + } + + private var dayLabels: [String] { + guard let monthDate = selectedMonthDate, + let range = calendar.range(of: .day, in: .month, for: monthDate) + else { + return ["全部"] + } + return ["全部"] + range.map { "\($0)日" } + } + + private var selectedYearValue: Int? { + Int(selectedYear.replacingOccurrences(of: "年", with: "")) + } + + private var selectedMonthValue: Int? { + Int(selectedMonth.replacingOccurrences(of: "月", with: "")) + } + + private var selectedDayValue: Int? { + guard selectedDay != "全部" else { return nil } + return Int(selectedDay.replacingOccurrences(of: "日", with: "")) + } + + private var selectedMonthDate: Date? { + guard let selectedYearValue, let selectedMonthValue else { return nil } + var components = DateComponents() + components.year = selectedYearValue + components.month = selectedMonthValue + components.day = 1 + return calendar.date(from: components) + } + + private var filteredDetails: [DailyUsageDetail] { + guard let selectedYearValue, let selectedMonthValue else { return [] } + return details.filter { + let components = calendar.dateComponents([.year, .month, .day], from: $0.date) + guard components.year == selectedYearValue, components.month == selectedMonthValue else { + return false + } + if let selectedDayValue { + return components.day == selectedDayValue + } + return true + } + } + + private var selectedRangeTotal: Int { + filteredDetails.reduce(0) { $0 + $1.totalTokens } + } + + var body: some View { + VStack(spacing: 0) { + HStack(spacing: 14) { + Button(action: onBack) { + Label("返回", systemImage: "chevron.left") + .labelStyle(.titleAndIcon) + } + .buttonStyle(GlassButtonStyle()) + + VStack(alignment: .leading, spacing: 3) { + Text("每日用量详情") + .font(.system(size: 24, weight: .bold, design: .rounded)) + Text("按本地日期汇总输入、输出、缓存和每个工具的 Tokens") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(.secondary) + } + + Spacer() + + VStack(alignment: .trailing, spacing: 3) { + Text("筛选日期") + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(.secondary) + HStack(spacing: 12) { + DateFilterControl( + year: $selectedYear, + month: $selectedMonth, + day: $selectedDay, + yearOptions: yearOptions, + monthOptions: monthLabels, + dayOptions: dayLabels + ) + VStack(alignment: .trailing, spacing: 1) { + Text("合计") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(.secondary) + Text(TokenFormatter.format(selectedRangeTotal)) + .font(.system(size: 18, weight: .bold, design: .rounded)) + .monospacedDigit() + } + } + } + } + .padding(.horizontal, 26) + .frame(height: 78) + .background(.ultraThinMaterial) + .overlay(alignment: .bottom) { + Rectangle() + .fill(.white.opacity(0.08)) + .frame(height: 1) + } + + ScrollView(showsIndicators: false) { + VStack(spacing: 14) { + DailyUsageHeader() + ForEach(filteredDetails) { detail in + DailyUsageRow(detail: detail) + } + if filteredDetails.isEmpty { + EmptyDailyUsageState() + } + } + .padding(.horizontal, 26) + .padding(.top, 22) + .padding(.bottom, 28) + } + } + .onAppear(perform: ensureDateSelection) + .onChange(of: summary.cards.totalTokens) { _, _ in + ensureDateSelection() + } + .onChange(of: selectedYear) { _, _ in + ensureDaySelection() + } + .onChange(of: selectedMonth) { _, newValue in + guard !newValue.isEmpty else { return } + ensureDaySelection() + } + .onChange(of: selectedDay) { _, newValue in + if !dayLabels.contains(newValue) { + selectedDay = "全部" + } + } + } + + private func ensureDateSelection() { + let latestDate = details.first?.date ?? summary.generatedAt + let components = calendar.dateComponents([.year, .month], from: latestDate) + let fallbackYear = "\(components.year ?? calendar.component(.year, from: summary.generatedAt))年" + let fallbackMonth = "\(components.month ?? calendar.component(.month, from: summary.generatedAt))月" + + if selectedYear.isEmpty || !yearOptions.contains(selectedYear) { + selectedYear = yearOptions.first ?? fallbackYear + } + if selectedMonth.isEmpty || !monthLabels.contains(selectedMonth) { + selectedMonth = fallbackMonth + } + ensureDaySelection() + } + + private func ensureDaySelection() { + if selectedDay.isEmpty || !dayLabels.contains(selectedDay) { + selectedDay = "全部" + } + } +} + +private struct DailyUsageHeader: View { + var body: some View { + HStack(spacing: 18) { + Text("日期").frame(width: DailyUsageColumnLayout.date, alignment: .center) + Text("总量").frame(width: DailyUsageColumnLayout.total, alignment: .center) + Text("输入").frame(width: DailyUsageColumnLayout.metric, alignment: .center) + Text("输出").frame(width: DailyUsageColumnLayout.metric, alignment: .center) + Text("缓存").frame(width: DailyUsageColumnLayout.metric, alignment: .center) + Text("工具分布").frame(maxWidth: .infinity, alignment: .center) + } + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.secondary) + .padding(.horizontal, 22) + .frame(height: 44) + .background(.white.opacity(0.055), in: RoundedRectangle(cornerRadius: 14, style: .continuous)) + .overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).stroke(.white.opacity(0.08))) + } +} + +private struct DateFilterControl: View { + @Binding var year: String + @Binding var month: String + @Binding var day: String + let yearOptions: [String] + let monthOptions: [String] + let dayOptions: [String] + + var body: some View { + HStack(spacing: 0) { + PrettyFilterMenu(title: "年", selection: $year, options: yearOptions) + .frame(width: 104) + Divider() + .frame(height: 22) + .overlay(.white.opacity(0.10)) + PrettyFilterMenu(title: "月", selection: $month, options: monthOptions) + .frame(width: 86) + Divider() + .frame(height: 22) + .overlay(.white.opacity(0.10)) + PrettyFilterMenu(title: "日", selection: $day, options: dayOptions) + .frame(width: 86) + } + .frame(height: 42) + .background { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(.thinMaterial) + .overlay { + LinearGradient( + colors: [.white.opacity(0.20), .white.opacity(0.04)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + } + } + .overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).stroke(.white.opacity(0.18))) + .shadow(color: .black.opacity(0.12), radius: 12, y: 8) + } +} + +private struct PrettyFilterMenu: View { + let title: String + @Binding var selection: String + let options: [String] + @State private var showingOptions = false + + var body: some View { + Button { + showingOptions.toggle() + } label: { + HStack(spacing: 7) { + Text(title) + .font(.system(size: 11, weight: .bold)) + .foregroundStyle(.secondary) + Text(selection) + .font(.system(size: 14, weight: .bold, design: .rounded)) + .foregroundStyle(.primary) + .lineLimit(1) + .minimumScaleFactor(0.82) + Image(systemName: "chevron.down") + .font(.system(size: 9, weight: .black)) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .popover(isPresented: $showingOptions, arrowEdge: .bottom) { + DropdownOptionList(selection: selection, options: options) { option in + selection = option + showingOptions = false + } + } + } +} + +private struct EmptyDailyUsageState: View { + var body: some View { + VStack(spacing: 10) { + Image(systemName: "calendar.badge.exclamationmark") + .font(.system(size: 28, weight: .semibold)) + .foregroundStyle(.secondary) + Text("这个日期范围还没有统计数据") + .font(.system(size: 16, weight: .bold)) + Text("TokenLens 会继续扫描本地日志,有新记录后这里会自动刷新。") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 42) + } +} + +private struct DailyUsageRow: View { + let detail: DailyUsageDetail + @State private var hovering = false + + var body: some View { + GlassCard { + HStack(spacing: 18) { + VStack(alignment: .center, spacing: 4) { + Text(detail.date.formatted(.dateTime.month(.twoDigits).day(.twoDigits))) + .font(.system(size: 19, weight: .bold, design: .rounded)) + Text(detail.date.formatted(.dateTime.weekday(.wide))) + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(.secondary) + } + .frame(width: DailyUsageColumnLayout.date, alignment: .center) + + metric(TokenFormatter.format(detail.totalTokens), tint: .primary, width: DailyUsageColumnLayout.total) + metric(TokenFormatter.format(detail.inputTokens), tint: .blue, width: DailyUsageColumnLayout.metric) + metric(TokenFormatter.format(detail.outputTokens), tint: .green, width: DailyUsageColumnLayout.metric) + metric(TokenFormatter.format(detail.cachedTokens), tint: .orange, width: DailyUsageColumnLayout.metric) + + ProviderUsageDistribution(detail: detail) + .frame(maxWidth: .infinity) + } + } + .scaleEffect(hovering ? 1.006 : 1) + .animation(.smooth(duration: 0.18), value: hovering) + .onHover { hovering = $0 } + } + + private func metric(_ text: String, tint: Color, width: CGFloat) -> some View { + Text(text) + .font(.system(size: 18, weight: .bold, design: .rounded)) + .foregroundStyle(tint) + .monospacedDigit() + .lineLimit(1) + .minimumScaleFactor(0.76) + .frame(width: width, alignment: .center) + } +} + +private enum DailyUsageColumnLayout { + static let date: CGFloat = 112 + static let total: CGFloat = 98 + static let metric: CGFloat = 88 +} + +private struct ProviderUsageDistribution: View { + let detail: DailyUsageDetail + + var body: some View { + VStack(alignment: .leading, spacing: 9) { + GeometryReader { proxy in + HStack(spacing: 3) { + ForEach(activeProviders, id: \.self) { provider in + RoundedRectangle(cornerRadius: 5, style: .continuous) + .fill(provider.color.gradient) + .frame(width: segmentWidths(totalWidth: proxy.size.width)[provider, default: 0]) + } + } + .frame(width: proxy.size.width, alignment: .leading) + .clipped() + } + .frame(height: 11) + .background(.white.opacity(0.055), in: RoundedRectangle(cornerRadius: 5, style: .continuous)) + .overlay(RoundedRectangle(cornerRadius: 5, style: .continuous).stroke(.white.opacity(0.08))) + + HStack(spacing: 10) { + ForEach(Provider.allCases, id: \.self) { provider in + providerCell(provider) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background(.white.opacity(0.045), in: RoundedRectangle(cornerRadius: 15, style: .continuous)) + .overlay(RoundedRectangle(cornerRadius: 15, style: .continuous).stroke(.white.opacity(0.09))) + } + + private func providerCell(_ provider: Provider) -> some View { + let value = detail.providerTotals[provider, default: 0] + let percent = detail.totalTokens > 0 ? Int((Double(value) / Double(detail.totalTokens) * 100).rounded()) : 0 + return HStack(spacing: 7) { + Circle() + .fill(provider.color) + .frame(width: 7, height: 7) + VStack(alignment: .leading, spacing: 1) { + Text(provider.rawValue) + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(.secondary) + Text("\(TokenFormatter.format(value)) · \(percent)%") + .font(.system(size: 11, weight: .bold, design: .rounded)) + .monospacedDigit() + .lineLimit(1) + .minimumScaleFactor(0.72) + } + .frame(minWidth: 0, alignment: .leading) + } + } + + private var activeProviders: [Provider] { + let providers = Provider.allCases.filter { detail.providerTotals[$0, default: 0] > 0 } + return providers.isEmpty ? Provider.allCases : providers + } + + private func segmentWidths(totalWidth: CGFloat) -> [Provider: CGFloat] { + let providers = activeProviders + guard totalWidth > 0, !providers.isEmpty else { return [:] } + + let spacing: CGFloat = 3 + let totalSpacing = spacing * CGFloat(max(providers.count - 1, 0)) + let availableWidth = max(0, totalWidth - totalSpacing) + + guard detail.totalTokens > 0 else { + let width = availableWidth / CGFloat(providers.count) + return Dictionary(uniqueKeysWithValues: providers.map { ($0, width) }) + } + + let minWidth: CGFloat = 8 + let minTotal = minWidth * CGFloat(providers.count) + if minTotal >= availableWidth { + let width = availableWidth / CGFloat(providers.count) + return Dictionary(uniqueKeysWithValues: providers.map { ($0, width) }) + } + + let remainingWidth = availableWidth - minTotal + return Dictionary(uniqueKeysWithValues: providers.map { provider in + let value = CGFloat(detail.providerTotals[provider, default: 0]) + let proportional = remainingWidth * value / CGFloat(detail.totalTokens) + return (provider, minWidth + proportional) + }) + } +} + +struct StatusPill: View { + let isRefreshing: Bool + let phase: Double + + var body: some View { + HStack(spacing: 8) { + Circle() + .fill(isRefreshing ? Color.orange : Color.green) + .frame(width: 8, height: 8) + .shadow(color: isRefreshing ? .orange.opacity(0.45) : .green.opacity(0.35), radius: 6) + Text(isRefreshing ? "正在刷新" : "每 15 秒自动刷新") + .font(.system(size: 13, weight: .semibold)) + } + .padding(.horizontal, 12) + .frame(height: 30) + .background(.thinMaterial, in: Capsule()) + .overlay(Capsule().stroke(.white.opacity(0.12))) + } +} + +struct MetricGrid: View { + let cards: SummaryCards + + var body: some View { + HStack(spacing: 18) { + MetricCard(title: "今日 Tokens", value: TokenFormatter.format(cards.todayTokens), tint: .blue, icon: "square.stack.3d.down.right.fill", footnote: "实时 本地今日已统计", delay: 0) + MetricCard(title: "本月 Tokens", value: TokenFormatter.format(cards.monthTokens), tint: .green, icon: "sparkles", footnote: "月度 按本地时区汇总", delay: 0.06) + MetricCard(title: "历史总量", value: TokenFormatter.format(cards.totalTokens), tint: .purple, icon: "checkmark", footnote: "全部 已扫描 \(cards.sessionCount) 个会话", delay: 0.12) + MetricCard(title: "缓存命中", value: "\(cards.cacheHitRate)%", tint: .orange, icon: "arrow.up.right", footnote: "缓存 来自缓存上下文", delay: 0.18) + } + } +} + +struct MetricCard: View { + let title: String + let value: String + let tint: Color + let icon: String + let footnote: String + let delay: Double + @State private var hovering = false + + var body: some View { + GlassCard(padding: 18, cornerRadius: 22) { + HStack(spacing: 16) { + ZStack { + Circle() + .fill(tint.gradient) + Circle() + .stroke(.white.opacity(0.30), lineWidth: 1) + Image(systemName: icon) + .font(.system(size: 23, weight: .bold)) + .foregroundStyle(.white.opacity(0.92)) + } + .frame(width: 54, height: 54) + .shadow(color: tint.opacity(0.24), radius: 12, y: 6) + .rotation3DEffect(.degrees(hovering ? 6 : 0), axis: (x: 0, y: 1, z: 0)) + + VStack(alignment: .leading, spacing: 6) { + HStack(spacing: 5) { + Text(title) + Image(systemName: "info.circle") + } + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(.secondary) + Text(value) + .font(.system(size: 32, weight: .bold, design: .rounded)) + .monospacedDigit() + .lineLimit(1) + .minimumScaleFactor(0.72) + .contentTransition(.numericText()) + Text(footnote) + .font(.system(size: 13, weight: .semibold)) + .lineLimit(1) + .minimumScaleFactor(0.80) + .foregroundStyle(tint, .secondary) + } + Spacer(minLength: 0) + } + } + .scaleEffect(hovering ? 1.01 : 1) + .offset(y: hovering ? -2 : 0) + .animation(.smooth(duration: 0.18), value: hovering) + .frame(height: 124) + .onHover { hovering = $0 } + } +} + +struct GlassPanel: View { + let title: String + @ViewBuilder let content: Content + @ViewBuilder let accessory: Accessory + + var body: some View { + GlassCard { + VStack(spacing: 14) { + HStack { + Text(title) + .font(.system(size: 21, weight: .bold, design: .rounded)) + Spacer() + accessory + } + content + } + } + } +} + +struct GlassCard: View { + @ViewBuilder let content: Content + var padding: CGFloat = 22 + var cornerRadius: CGFloat = 24 + @State private var hovering = false + + init( + padding: CGFloat = 22, + cornerRadius: CGFloat = 24, + @ViewBuilder content: () -> Content + ) { + self.padding = padding + self.cornerRadius = cornerRadius + self.content = content() + } + + var body: some View { + content + .padding(padding) + .background { + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + .fill(.ultraThinMaterial) + .overlay { + LinearGradient( + colors: [.white.opacity(0.18), .white.opacity(0.04), .clear], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) + } + .overlay { + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + .stroke(.white.opacity(hovering ? 0.28 : 0.16), lineWidth: 1) + } + .shadow(color: .black.opacity(0.22), radius: hovering ? 32 : 28, y: hovering ? 20 : 18) + } + .animation(.smooth(duration: 0.18), value: hovering) + .onHover { hovering = $0 } + } +} + +struct NativeMenuPicker: View { + @Binding var selection: String + let options: [String] + @State private var showingOptions = false + + var body: some View { + Button { + showingOptions.toggle() + } label: { + HStack(spacing: 9) { + Text(selection) + .lineLimit(1) + .minimumScaleFactor(0.84) + Spacer() + ZStack { + Circle() + .fill(.white.opacity(0.10)) + .frame(width: 20, height: 20) + Image(systemName: showingOptions ? "chevron.up" : "chevron.down") + .font(.system(size: 9, weight: .black)) + .foregroundStyle(.secondary) + } + } + .font(.system(size: 14, weight: .bold, design: .rounded)) + .padding(.horizontal, 14) + .frame(height: 42) + .background { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(.thinMaterial) + .overlay { + LinearGradient( + colors: [.white.opacity(0.22), .white.opacity(0.05)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + } + } + .overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).stroke(.white.opacity(0.20))) + .shadow(color: .black.opacity(0.12), radius: 10, y: 7) + } + .buttonStyle(.plain) + .popover(isPresented: $showingOptions, arrowEdge: .bottom) { + DropdownOptionList(selection: selection, options: options) { option in + selection = option + showingOptions = false + } + } + } +} + +private struct DropdownOptionList: View { + let selection: String + let options: [String] + let onSelect: (String) -> Void + + var body: some View { + VStack(spacing: 4) { + if options.isEmpty { + Text("暂无选项") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, minHeight: 34) + } else { + ForEach(options, id: \.self) { option in + Button { + onSelect(option) + } label: { + HStack(spacing: 9) { + Text(option) + .lineLimit(1) + Spacer() + if option == selection { + Image(systemName: "checkmark") + .font(.system(size: 11, weight: .bold)) + .foregroundStyle(.blue) + } + } + .font(.system(size: 13, weight: option == selection ? .bold : .semibold, design: .rounded)) + .padding(.horizontal, 10) + .frame(height: 34) + .background( + option == selection ? Color.blue.opacity(0.16) : Color.white.opacity(0.001), + in: RoundedRectangle(cornerRadius: 10, style: .continuous) + ) + } + .buttonStyle(.plain) + } + } + } + .padding(6) + .frame(minWidth: 136) + .background(.ultraThinMaterial) + .preferredColorScheme(.dark) + } +} + +struct GlassButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.system(size: 15, weight: .semibold)) + .padding(.horizontal, 16) + .frame(height: 42) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 13, style: .continuous)) + .overlay(RoundedRectangle(cornerRadius: 13).stroke(.white.opacity(0.16))) + .scaleEffect(configuration.isPressed ? 0.97 : 1) + .animation(.smooth(duration: 0.16), value: configuration.isPressed) + } +} diff --git a/NativeTokenLens/Sources/TokenLens/FloatingTokenWindow.swift b/NativeTokenLens/Sources/TokenLens/FloatingTokenWindow.swift new file mode 100644 index 0000000..8a8a86d --- /dev/null +++ b/NativeTokenLens/Sources/TokenLens/FloatingTokenWindow.swift @@ -0,0 +1,248 @@ +import AppKit +import SwiftUI +import TokenLensCore + +struct FloatingTokenWindow: View { + @ObservedObject var store: UsageStore + let onSizeChange: (CGSize) -> Void + @Environment(\.colorScheme) private var colorScheme + @StateObject private var settings = AppSettings.shared + @State private var expanded = false + @State private var collapseTask: Task? + + private var todayText: String { + TokenFormatter.format(store.summary.cards.todayTokens) + } + + private var targetWidth: CGFloat { + let digitWidth = CGFloat(max(todayText.count - 3, 0)) * 11 + return expanded ? max(338, 282 + digitWidth) : min(220, max(142, 120 + digitWidth)) + } + + private var visibleProviders: [Provider] { + Provider.allCases.filter { + (store.summary.todayProviderTotals[$0]?.totalTokens ?? 0) > 0 + } + } + + private var providerRowCount: Int { + max(visibleProviders.count, 1) + } + + private var activeProviderSignature: String { + visibleProviders.map(\.rawValue).joined(separator: "|") + } + + private var targetHeight: CGFloat { + expanded ? 84 + CGFloat(providerRowCount) * 62 + CGFloat(max(providerRowCount - 1, 0)) * 8 : 36 + } + private var cornerRadius: CGFloat { expanded ? 18 : targetHeight / 2 } + private var expansionAnimation: Animation { + .spring(response: 0.38, dampingFraction: 0.92, blendDuration: 0.10) + } + + var body: some View { + ZStack(alignment: .topLeading) { + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + .fill(panelFill) + .overlay { + LinearGradient( + colors: [.white.opacity(colorScheme == .dark ? 0.18 : 0.46), .white.opacity(0.04)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) + } + .overlay { + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + .stroke(.white.opacity(colorScheme == .dark ? 0.18 : 0.54), lineWidth: 1) + } + .shadow(color: .black.opacity(colorScheme == .dark ? 0.32 : 0.16), radius: expanded ? 16 : 10, y: expanded ? 10 : 5) + + content + .padding(expanded ? EdgeInsets(top: 14, leading: 12, bottom: 14, trailing: 12) : EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7)) + } + .frame(width: targetWidth, height: targetHeight, alignment: .topLeading) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) + .animation(expansionAnimation, value: expanded) + .animation(.smooth(duration: 0.22), value: store.summary.cards.totalTokens) + .contentShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) + .onTapGesture { + toggleExpanded() + } + .onChange(of: settings.autoCollapseSeconds) { _, _ in + if expanded { + scheduleAutoCollapse() + } + } + .onAppear(perform: reportSize) + .onChange(of: expanded) { _, _ in reportSize() } + .onChange(of: todayText) { _, _ in reportSize() } + .onChange(of: activeProviderSignature) { _, _ in reportSize() } + .preferredColorScheme(settings.floatingAppearance.colorScheme) + } + + private var panelFill: Color { + if colorScheme == .dark { + return Color(red: 0.08, green: 0.09, blue: 0.11).opacity(0.86) + } + return Color.white.opacity(0.88) + } + + @ViewBuilder + private var content: some View { + VStack(alignment: .leading, spacing: expanded ? 12 : 0) { + if expanded { + HStack(spacing: 8) { + FloatingTrendIcon() + .frame(width: 32, height: 32) + VStack(alignment: .leading, spacing: 1) { + Text("今日 Tokens") + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(.secondary) + Text(todayText) + .font(.system(size: 22, weight: .bold, design: .rounded)) + .foregroundStyle(.primary) + .monospacedDigit() + .contentTransition(.numericText()) + } + Spacer(minLength: 0) + VStack(alignment: .trailing, spacing: 1) { + Text("实时") + .font(.system(size: 11, weight: .bold)) + Text("15 秒刷新") + .font(.system(size: 10, weight: .medium)) + .foregroundStyle(.secondary) + } + } + + VStack(spacing: 8) { + if visibleProviders.isEmpty { + Text("今日暂无工具流量") + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity) + .frame(height: 54) + .background(.white.opacity(0.06), in: RoundedRectangle(cornerRadius: 11, style: .continuous)) + } else { + ForEach(visibleProviders, id: \.self) { provider in + FloatingProviderRow( + provider: provider, + bucket: store.summary.todayProviderTotals[provider] ?? TokenBucket(provider: provider) + ) + } + } + } + .frame(maxWidth: .infinity) + } else { + HStack(spacing: 6) { + FloatingTrendIcon() + .frame(width: 22, height: 22) + Text("今日") + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(.secondary) + Text(todayText) + .font(.system(size: 15, weight: .bold, design: .rounded)) + .foregroundStyle(.primary) + .monospacedDigit() + .contentTransition(.numericText()) + Spacer(minLength: 0) + } + } + } + } + + private func toggleExpanded() { + collapseTask?.cancel() + withAnimation(expansionAnimation) { + expanded.toggle() + } + if expanded { + scheduleAutoCollapse() + } + } + + private func scheduleAutoCollapse() { + collapseTask?.cancel() + let delay = UInt64(max(1, settings.autoCollapseSeconds)) * 1_000_000_000 + collapseTask = Task { @MainActor in + try? await Task.sleep(nanoseconds: delay) + guard !Task.isCancelled else { return } + withAnimation(expansionAnimation) { + expanded = false + } + } + } + + private func reportSize() { + let size = CGSize(width: targetWidth, height: targetHeight) + DispatchQueue.main.async { + onSizeChange(size) + } + } +} + +private struct FloatingTrendIcon: View { + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 9, style: .continuous) + .fill( + LinearGradient( + colors: [.blue.opacity(0.88), .purple.opacity(0.82)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .overlay { + RoundedRectangle(cornerRadius: 9, style: .continuous) + .stroke(.white.opacity(0.26), lineWidth: 1) + } + Image(systemName: "chart.line.uptrend.xyaxis") + .font(.system(size: 14, weight: .bold)) + .foregroundStyle(.white) + } + .shadow(color: .blue.opacity(0.24), radius: 8, y: 4) + } +} + +private struct FloatingProviderRow: View { + let provider: Provider + let bucket: TokenBucket + + var body: some View { + VStack(spacing: 5) { + HStack(spacing: 8) { + Circle() + .fill(provider.color) + .frame(width: 7, height: 7) + Text(provider.rawValue) + .font(.system(size: 12, weight: .bold)) + Spacer() + Text(TokenFormatter.format(bucket.totalTokens)) + .font(.system(size: 13, weight: .bold, design: .rounded)) + .monospacedDigit() + } + HStack(spacing: 8) { + detail("输入", bucket.inputTokens) + detail("输出", bucket.outputTokens) + detail("缓存", bucket.cachedTokens) + } + } + .padding(.horizontal, 8) + .padding(.vertical, 7) + .background(provider.color.opacity(0.08), in: RoundedRectangle(cornerRadius: 11, style: .continuous)) + .overlay(RoundedRectangle(cornerRadius: 11, style: .continuous).stroke(provider.color.opacity(0.16))) + } + + private func detail(_ label: String, _ value: Int) -> some View { + HStack(spacing: 3) { + Text(label) + .foregroundStyle(.secondary) + Text(TokenFormatter.format(value)) + .fontWeight(.semibold) + .monospacedDigit() + } + .font(.system(size: 10)) + .lineLimit(1) + } +} diff --git a/NativeTokenLens/Sources/TokenLens/SettingsView.swift b/NativeTokenLens/Sources/TokenLens/SettingsView.swift new file mode 100644 index 0000000..ca8ea3d --- /dev/null +++ b/NativeTokenLens/Sources/TokenLens/SettingsView.swift @@ -0,0 +1,114 @@ +import SwiftUI + +struct SettingsView: View { + @StateObject private var settings = AppSettings.shared + + var body: some View { + VStack(alignment: .leading, spacing: 18) { + VStack(alignment: .leading, spacing: 4) { + Text("TokenLens 设置") + .font(.system(size: 22, weight: .bold, design: .rounded)) + Text("控制桌面辅助入口和菜单栏显示。") + .foregroundStyle(.secondary) + } + + VStack(spacing: 12) { + SettingsToggleRow( + title: "显示悬浮窗", + subtitle: "在桌面上显示总 Tokens 消耗,鼠标放上去展开详情。", + systemImage: "rectangle.on.rectangle", + isOn: $settings.showFloatingWindow + ) + SettingsToggleRow( + title: "显示状态栏提示窗", + subtitle: "在 macOS 菜单栏显示当前总 Tokens 数量。", + systemImage: "menubar.rectangle", + isOn: $settings.showStatusBarIndicator + ) + SettingsPickerRow( + title: "自动回收", + subtitle: "悬浮窗和状态栏详情展开后的自动收起时间。", + systemImage: "timer", + selection: $settings.autoCollapseSeconds, + options: AutoCollapseDelay.allCases.map { ($0.rawValue, $0.label) } + ) + SettingsPickerRow( + title: "悬浮窗皮肤", + subtitle: "控制悬浮窗亮色、暗色或跟随系统主题。", + systemImage: "circle.lefthalf.filled", + selection: Binding( + get: { settings.floatingAppearance.rawValue }, + set: { settings.floatingAppearance = FloatingAppearance(rawValue: $0) ?? .system } + ), + options: FloatingAppearance.allCases.map { ($0.rawValue, $0.rawValue) } + ) + } + Spacer() + } + .padding(26) + .frame(width: 520, height: 430) + } +} + +private struct SettingsToggleRow: View { + let title: String + let subtitle: String + let systemImage: String + @Binding var isOn: Bool + + var body: some View { + HStack(spacing: 12) { + Image(systemName: systemImage) + .font(.system(size: 18, weight: .semibold)) + .frame(width: 34, height: 34) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + VStack(alignment: .leading, spacing: 3) { + Text(title) + .font(.system(size: 14, weight: .semibold)) + Text(subtitle) + .font(.system(size: 12)) + .foregroundStyle(.secondary) + } + Spacer() + Toggle("", isOn: $isOn) + .toggleStyle(.switch) + .labelsHidden() + } + .padding(12) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous)) + } +} + +private struct SettingsPickerRow: View { + let title: String + let subtitle: String + let systemImage: String + @Binding var selection: Value + let options: [(Value, String)] + + var body: some View { + HStack(spacing: 12) { + Image(systemName: systemImage) + .font(.system(size: 18, weight: .semibold)) + .frame(width: 34, height: 34) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + VStack(alignment: .leading, spacing: 3) { + Text(title) + .font(.system(size: 14, weight: .semibold)) + Text(subtitle) + .font(.system(size: 12)) + .foregroundStyle(.secondary) + } + Spacer() + Picker("", selection: $selection) { + ForEach(options, id: \.0) { value, label in + Text(label).tag(value) + } + } + .pickerStyle(.segmented) + .frame(width: 190) + } + .padding(12) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous)) + } +} diff --git a/NativeTokenLens/Sources/TokenLens/TokenLensApp.swift b/NativeTokenLens/Sources/TokenLens/TokenLensApp.swift new file mode 100644 index 0000000..d812379 --- /dev/null +++ b/NativeTokenLens/Sources/TokenLens/TokenLensApp.swift @@ -0,0 +1,446 @@ +import SwiftUI +import AppKit +import Combine +import TokenLensCore + +@main +struct TokenLensApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + + var body: some Scene { + Settings { + SettingsView() + .preferredColorScheme(.dark) + } + .commands { + CommandGroup(replacing: .newItem) {} + } + } +} + +final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { + private var dashboardWindow: NSWindow? + private var floatingPanel: NSPanel? + private var statusItem: NSStatusItem? + private var statusPopover: NSPopover? + private var statusPopoverCloseTask: Task? + private var cancellables: Set = [] + private var isTerminating = false + + func applicationDidFinishLaunching(_ notification: Notification) { + NSApp.setActivationPolicy(.regular) + UsageStore.shared.start() + bindAppState() + updateFloatingPanelVisibility() + updateStatusItemVisibility() + } + + func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + Task { @MainActor in + showDashboardWindow() + } + return false + } + + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + isTerminating = true + return .terminateNow + } + + @MainActor + func showDashboardWindow() { + closeStatusPopover() + + if let dashboardWindow { + NSApp.activate(ignoringOtherApps: true) + if dashboardWindow.isMiniaturized { + dashboardWindow.deminiaturize(nil) + } + dashboardWindow.makeKeyAndOrderFront(nil) + dashboardWindow.orderFrontRegardless() + return + } + + let hostingView = NSHostingView( + rootView: DashboardView() + .frame(minWidth: 1180, minHeight: 760) + .preferredColorScheme(.dark) + ) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 1280, height: 860), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, + defer: false + ) + window.title = "TokenLens" + window.identifier = NSUserInterfaceItemIdentifier("main-dashboard-window") + window.titleVisibility = .hidden + window.titlebarAppearsTransparent = true + window.isMovableByWindowBackground = false + window.backgroundColor = .clear + window.isOpaque = false + window.minSize = NSSize(width: 1180, height: 760) + window.contentView = hostingView + window.delegate = self + window.center() + + dashboardWindow = window + NSApp.activate(ignoringOtherApps: true) + window.makeKeyAndOrderFront(nil) + window.orderFrontRegardless() + } + + func windowWillClose(_ notification: Notification) { + if notification.object as? NSWindow === dashboardWindow { + dashboardWindow = nil + } + } + + func windowShouldClose(_ sender: NSWindow) -> Bool { + guard sender === dashboardWindow, !isTerminating else { + return true + } + sender.miniaturize(nil) + return false + } + + @MainActor + private func bindAppState() { + AppSettings.shared.$showFloatingWindow + .sink { [weak self] isVisible in self?.updateFloatingPanelVisibility(isVisible: isVisible) } + .store(in: &cancellables) + AppSettings.shared.$showStatusBarIndicator + .sink { [weak self] isVisible in self?.updateStatusItemVisibility(isVisible: isVisible) } + .store(in: &cancellables) + UsageStore.shared.$summary + .sink { [weak self] _ in self?.updateStatusTitle() } + .store(in: &cancellables) + AppSettings.shared.$floatingAppearance + .sink { [weak self] _ in + self?.applyFloatingPanelAppearance() + self?.applyStatusPopoverAppearance() + } + .store(in: &cancellables) + } + + @MainActor + private func updateFloatingPanelVisibility(isVisible: Bool = AppSettings.shared.showFloatingWindow) { + if isVisible { + if floatingPanel == nil { + installFloatingPanel() + } + floatingPanel?.orderFrontRegardless() + } else { + floatingPanel?.orderOut(nil) + } + } + + @MainActor + private func installFloatingPanel() { + let initialFloatingSize = NSSize(width: 164, height: 36) + let panel = NSPanel( + contentRect: NSRect(origin: NSPoint(x: 80, y: 80), size: initialFloatingSize), + styleMask: [.borderless, .nonactivatingPanel], + backing: .buffered, + defer: false + ) + panel.title = "TokenLens Floating" + panel.level = .floating + panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] + panel.isOpaque = false + panel.backgroundColor = .clear + panel.hasShadow = false + panel.isMovableByWindowBackground = true + panel.hidesOnDeactivate = false + panel.appearance = AppSettings.shared.floatingAppearance.nsAppearance + + let hostingView = TransparentHostingView( + rootView: FloatingTokenWindow(store: UsageStore.shared) { [weak self] size in + Task { @MainActor in + self?.resizeFloatingPanel(to: size) + } + } + .background(Color.clear) + ) + makeTransparent(hostingView) + panel.contentView = hostingView + if let contentView = panel.contentView { + makeTransparent(contentView) + } + + if let screenFrame = NSScreen.main?.visibleFrame { + let x = screenFrame.maxX - 360 + let y = screenFrame.maxY - 170 + panel.setFrameOrigin(NSPoint(x: x, y: y)) + } + panel.orderFrontRegardless() + floatingPanel = panel + } + + @MainActor + private func resizeFloatingPanel(to size: CGSize) { + guard let panel = floatingPanel else { return } + let newSize = NSSize(width: ceil(size.width), height: ceil(size.height)) + guard abs(panel.frame.width - newSize.width) > 0.5 || abs(panel.frame.height - newSize.height) > 0.5 else { + return + } + let currentFrame = panel.frame + let newFrame = NSRect( + x: currentFrame.minX, + y: currentFrame.maxY - newSize.height, + width: newSize.width, + height: newSize.height + ) + panel.setFrame(newFrame, display: true) + } + + @MainActor + private func updateStatusItemVisibility(isVisible: Bool = AppSettings.shared.showStatusBarIndicator) { + if isVisible { + if statusItem == nil { + let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + item.button?.target = self + item.button?.action = #selector(statusItemClicked) + item.button?.image = NSImage(systemSymbolName: "chart.line.uptrend.xyaxis", accessibilityDescription: "Tokens") + item.button?.imagePosition = .imageLeading + statusItem = item + } + updateStatusTitle() + } else if let item = statusItem { + NSStatusBar.system.removeStatusItem(item) + statusItem = nil + } + } + + @MainActor + private func updateStatusTitle() { + guard let button = statusItem?.button else { return } + button.title = " \(TokenFormatter.format(UsageStore.shared.summary.cards.todayTokens))" + button.toolTip = "TokenLens 今日 Tokens:\(TokenFormatter.format(UsageStore.shared.summary.cards.todayTokens))" + } + + @objc + @MainActor + private func statusItemClicked() { + guard let button = statusItem?.button else { return } + if statusPopover?.isShown == true { + closeStatusPopover() + return + } + let popover = NSPopover() + popover.behavior = .transient + popover.contentSize = statusPopoverSize() + let controller = NSHostingController( + rootView: StatusBarDetailView(store: UsageStore.shared) + ) + makeTransparent(controller.view) + popover.contentViewController = controller + popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) + statusPopover = popover + applyStatusPopoverAppearance() + scheduleStatusPopoverClose() + } + + @MainActor + private func scheduleStatusPopoverClose() { + statusPopoverCloseTask?.cancel() + let delay = UInt64(max(1, AppSettings.shared.autoCollapseSeconds)) * 1_000_000_000 + statusPopoverCloseTask = Task { @MainActor in + try? await Task.sleep(nanoseconds: delay) + guard !Task.isCancelled else { return } + closeStatusPopover() + } + } + + @MainActor + private func closeStatusPopover() { + statusPopoverCloseTask?.cancel() + statusPopover?.close() + statusPopover = nil + } + + @MainActor + private func statusPopoverSize() -> NSSize { + let activeProviderCount = Provider.allCases.filter { + (UsageStore.shared.summary.todayProviderTotals[$0]?.totalTokens ?? 0) > 0 + }.count + let rowCount = max(activeProviderCount, 1) + return NSSize(width: 330, height: 128 + CGFloat(rowCount) * 54 + CGFloat(max(rowCount - 1, 0)) * 8) + } + + @MainActor + private func applyFloatingPanelAppearance() { + floatingPanel?.appearance = AppSettings.shared.floatingAppearance.nsAppearance + if let contentView = floatingPanel?.contentView { + makeTransparent(contentView) + } + } + + @MainActor + private func applyStatusPopoverAppearance() { + guard let popover = statusPopover else { return } + let appearance = AppSettings.shared.floatingAppearance.nsAppearance + popover.contentViewController?.view.window?.appearance = appearance + if let view = popover.contentViewController?.view { + makeTransparent(view) + } + } + + @MainActor + private func makeTransparent(_ view: NSView) { + view.wantsLayer = true + view.layer?.backgroundColor = NSColor.clear.cgColor + view.layer?.isOpaque = false + view.layer?.masksToBounds = false + } +} + +final class TransparentHostingView: NSHostingView { + override var isOpaque: Bool { false } + + required init(rootView: Content) { + super.init(rootView: rootView) + prepareForTransparentCompositing() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + nil + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + prepareForTransparentCompositing() + window?.isOpaque = false + window?.backgroundColor = .clear + window?.hasShadow = true + } + + override func draw(_ dirtyRect: NSRect) { + NSColor.clear.setFill() + dirtyRect.fill() + super.draw(dirtyRect) + } + + private func prepareForTransparentCompositing() { + wantsLayer = true + layer?.backgroundColor = NSColor.clear.cgColor + layer?.isOpaque = false + layer?.masksToBounds = false + } +} + +struct StatusBarDetailView: View { + @ObservedObject var store: UsageStore + @StateObject private var settings = AppSettings.shared + + private var visibleProviders: [Provider] { + Provider.allCases.filter { + (store.summary.todayProviderTotals[$0]?.totalTokens ?? 0) > 0 + } + } + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack(spacing: 10) { + Image(systemName: "chart.line.uptrend.xyaxis") + .font(.system(size: 20, weight: .bold)) + .foregroundStyle(.white) + .frame(width: 36, height: 36) + .background(.blue.gradient, in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + VStack(alignment: .leading, spacing: 2) { + Text("今日 Tokens") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.secondary) + Text(TokenFormatter.format(store.summary.cards.todayTokens)) + .font(.system(size: 24, weight: .bold, design: .rounded)) + .monospacedDigit() + } + Spacer() + Text("实时") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(.secondary) + } + + VStack(spacing: 8) { + if visibleProviders.isEmpty { + Text("今日暂无工具流量") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity) + .frame(height: 46) + .background(.white.opacity(0.06), in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + } else { + ForEach(visibleProviders, id: \.self) { provider in + StatusProviderRow( + provider: provider, + bucket: store.summary.todayProviderTotals[provider] ?? TokenBucket(provider: provider) + ) + } + } + } + + Button { + WindowActions.showMainWindow() + } label: { + Label("打开主界面", systemImage: "rectangle.grid.2x2") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + } + .padding(14) + .frame(width: 330) + .background(.ultraThinMaterial) + .preferredColorScheme(settings.floatingAppearance.colorScheme) + } +} + +private struct StatusProviderRow: View { + let provider: Provider + let bucket: TokenBucket + + var body: some View { + HStack(spacing: 9) { + Circle() + .fill(provider.color) + .frame(width: 8, height: 8) + VStack(alignment: .leading, spacing: 2) { + HStack { + Text(provider.rawValue) + .font(.system(size: 13, weight: .bold)) + Spacer() + Text(TokenFormatter.format(bucket.totalTokens)) + .font(.system(size: 13, weight: .bold, design: .rounded)) + .monospacedDigit() + } + Text("输入 \(TokenFormatter.format(bucket.inputTokens)) 输出 \(TokenFormatter.format(bucket.outputTokens)) 缓存 \(TokenFormatter.format(bucket.cachedTokens))") + .font(.system(size: 10, weight: .medium)) + .foregroundStyle(.secondary) + } + } + .padding(.horizontal, 10) + .padding(.vertical, 8) + .background(provider.color.opacity(0.10), in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + } +} + +struct WindowAccessor: NSViewRepresentable { + func makeNSView(context: Context) -> NSView { + let view = NSView() + DispatchQueue.main.async { + guard let window = view.window else { return } + window.title = "TokenLens" + window.identifier = NSUserInterfaceItemIdentifier("main-dashboard-window") + window.titleVisibility = .hidden + window.titlebarAppearsTransparent = true + window.isMovableByWindowBackground = false + window.backgroundColor = .clear + window.isOpaque = false + window.setContentSize(NSSize(width: 1280, height: 860)) + window.minSize = NSSize(width: 1180, height: 760) + } + return view + } + + func updateNSView(_ nsView: NSView, context: Context) {} +} diff --git a/NativeTokenLens/Sources/TokenLens/UsageStore.swift b/NativeTokenLens/Sources/TokenLens/UsageStore.swift new file mode 100644 index 0000000..06af2b7 --- /dev/null +++ b/NativeTokenLens/Sources/TokenLens/UsageStore.swift @@ -0,0 +1,45 @@ +import SwiftUI +import TokenLensCore + +@MainActor +final class UsageStore: ObservableObject { + static let shared = UsageStore() + + @Published var summary = UsageSummary.make(records: []) + @Published var isRefreshing = false + @Published var refreshCount = 0 + + private let scanner = TokenUsageScanner() + private var refreshTask: Task? + + private init() {} + + func start() { + guard refreshTask == nil else { return } + refreshTask = Task { [weak self] in + guard let self else { return } + await self.refresh() + while !Task.isCancelled { + try? await Task.sleep(for: .seconds(15)) + await self.refresh(silent: true) + } + } + } + + func refresh(silent: Bool = false) async { + if isRefreshing { return } + if !silent { isRefreshing = true } + let next = await scanner.scan() + if silent { + summary = next + refreshCount += 1 + isRefreshing = false + } else { + withAnimation(.smooth(duration: 0.45)) { + summary = next + refreshCount += 1 + isRefreshing = false + } + } + } +} diff --git a/NativeTokenLens/Sources/TokenLens/WindowActions.swift b/NativeTokenLens/Sources/TokenLens/WindowActions.swift new file mode 100644 index 0000000..5264eae --- /dev/null +++ b/NativeTokenLens/Sources/TokenLens/WindowActions.swift @@ -0,0 +1,69 @@ +import AppKit + +enum WindowActions { + @MainActor + static func zoomActiveWindow() { + guard let window = NSApp.keyWindow ?? NSApp.mainWindow else { return } + toggleMaximize(window) + } + + @MainActor + static func toggleMaximize(_ window: NSWindow) { + guard let visibleFrame = window.screen?.visibleFrame ?? NSScreen.main?.visibleFrame else { + return + } + + let currentFrame = window.frame + let isMaximized = + abs(currentFrame.minX - visibleFrame.minX) < 2 && + abs(currentFrame.minY - visibleFrame.minY) < 2 && + abs(currentFrame.width - visibleFrame.width) < 2 && + abs(currentFrame.height - visibleFrame.height) < 2 + + if isMaximized { + let restoredSize = NSSize( + width: min(max(window.minSize.width, 1280), visibleFrame.width), + height: min(max(window.minSize.height, 860), visibleFrame.height) + ) + let restoredFrame = NSRect( + x: visibleFrame.midX - restoredSize.width / 2, + y: visibleFrame.midY - restoredSize.height / 2, + width: restoredSize.width, + height: restoredSize.height + ) + window.setFrame(restoredFrame, display: true, animate: false) + } else { + window.setFrame(visibleFrame, display: true, animate: false) + } + } + + @MainActor + static func showMainWindow() { + if let appDelegate = NSApp.delegate as? AppDelegate { + appDelegate.showDashboardWindow() + return + } + + let mainWindow = NSApp.windows.first { window in + window.identifier?.rawValue == "main-dashboard-window" + } ?? NSApp.windows.first { window in + !(window is NSPanel) && window.title == "TokenLens" + } ?? NSApp.windows.first { window in + !(window is NSPanel) && !window.title.contains("设置") + } + + NSApp.windows + .filter { $0 !== mainWindow && $0.title.contains("设置") } + .forEach { $0.orderOut(nil) } + + NSApp.activate(ignoringOtherApps: true) + NSApp.unhide(nil) + if mainWindow?.isMiniaturized == true { + mainWindow?.deminiaturize(nil) + } + guard let mainWindow else { return } + mainWindow.makeKeyAndOrderFront(nil) + mainWindow.makeMain() + mainWindow.orderFrontRegardless() + } +} diff --git a/NativeTokenLens/Sources/TokenLensCore/MemoryPressure.swift b/NativeTokenLens/Sources/TokenLensCore/MemoryPressure.swift new file mode 100644 index 0000000..2d52104 --- /dev/null +++ b/NativeTokenLens/Sources/TokenLensCore/MemoryPressure.swift @@ -0,0 +1,7 @@ +import Darwin + +public enum MemoryPressure { + public static func relieve() { + _ = malloc_zone_pressure_relief(nil, 0) + } +} diff --git a/NativeTokenLens/Sources/TokenLensCore/Models.swift b/NativeTokenLens/Sources/TokenLensCore/Models.swift new file mode 100644 index 0000000..ab41a5a --- /dev/null +++ b/NativeTokenLens/Sources/TokenLensCore/Models.swift @@ -0,0 +1,206 @@ +import Foundation + +public enum Provider: String, CaseIterable, Codable, Hashable, Sendable { + case claude = "Claude" + case codex = "Codex" + case gemini = "Gemini" + case hermes = "Hermes" + case opencode = "OpenCode" +} + +public struct UsageRecord: Identifiable, Hashable, Sendable { + public let id: UUID + public let provider: Provider + public let source: String + public let sourcePath: String + public let timestamp: Date + public let sessionID: String + public let model: String + public let inputTokens: Int + public let outputTokens: Int + public let cachedTokens: Int + public let reasoningTokens: Int + public let toolTokens: Int + public let totalTokens: Int + + public init( + id: UUID = UUID(), + provider: Provider, + source: String, + sourcePath: String, + timestamp: Date, + sessionID: String, + model: String, + inputTokens: Int, + outputTokens: Int, + cachedTokens: Int, + reasoningTokens: Int, + toolTokens: Int, + explicitTotal: Int? = nil + ) { + self.id = id + self.provider = provider + self.source = source + self.sourcePath = sourcePath + self.timestamp = timestamp + self.sessionID = sessionID + self.model = model + self.inputTokens = inputTokens + self.outputTokens = outputTokens + self.cachedTokens = cachedTokens + self.reasoningTokens = reasoningTokens + self.toolTokens = toolTokens + self.totalTokens = explicitTotal ?? inputTokens + outputTokens + cachedTokens + reasoningTokens + toolTokens + } +} + +public struct TokenBucket: Hashable, Sendable { + public let provider: Provider? + public var inputTokens: Int = 0 + public var outputTokens: Int = 0 + public var cachedTokens: Int = 0 + public var reasoningTokens: Int = 0 + public var toolTokens: Int = 0 + public var totalTokens: Int = 0 + public var count: Int = 0 + + public init(provider: Provider? = nil) { + self.provider = provider + } + + public mutating func add(_ record: UsageRecord) { + add( + inputTokens: record.inputTokens, + outputTokens: record.outputTokens, + cachedTokens: record.cachedTokens, + reasoningTokens: record.reasoningTokens, + toolTokens: record.toolTokens, + totalTokens: record.totalTokens + ) + } + + public mutating func add( + inputTokens: Int, + outputTokens: Int, + cachedTokens: Int, + reasoningTokens: Int, + toolTokens: Int, + totalTokens: Int + ) { + self.inputTokens += inputTokens + self.outputTokens += outputTokens + self.cachedTokens += cachedTokens + self.reasoningTokens += reasoningTokens + self.toolTokens += toolTokens + self.totalTokens += totalTokens + count += 1 + } +} + +public struct SummaryCards: Hashable, Sendable { + public let todayTokens: Int + public let monthTokens: Int + public let totalTokens: Int + public let cacheHitRate: Int + public let sessionCount: Int +} + +public struct DailyPoint: Identifiable, Hashable, Sendable { + public let id = UUID() + public let date: Date + public let values: [Provider: Int] +} + +public enum TrendMode: String, CaseIterable, Sendable { + case daily = "每日" + case weekly = "每周" + case monthly = "每月" + case yearly = "每年" +} + +public struct ToolRow: Identifiable, Hashable, Sendable { + public let id = UUID() + public let provider: Provider + public let inputTokens: Int + public let outputTokens: Int + public let cachedTokens: Int + public let totalTokens: Int + public let trendTokens: [Int] +} + +public struct DailyUsageDetail: Identifiable, Hashable, Sendable { + public var id: Date { date } + + public let date: Date + public var inputTokens: Int + public var outputTokens: Int + public var cachedTokens: Int + public var totalTokens: Int + public var providerTotals: [Provider: Int] + + public init( + date: Date, + inputTokens: Int = 0, + outputTokens: Int = 0, + cachedTokens: Int = 0, + totalTokens: Int = 0, + providerTotals: [Provider: Int] = [:] + ) { + self.date = date + self.inputTokens = inputTokens + self.outputTokens = outputTokens + self.cachedTokens = cachedTokens + self.totalTokens = totalTokens + self.providerTotals = providerTotals + } + + public mutating func add(_ record: UsageRecord) { + add( + provider: record.provider, + inputTokens: record.inputTokens, + outputTokens: record.outputTokens, + cachedTokens: record.cachedTokens, + totalTokens: record.totalTokens + ) + } + + public mutating func add( + provider: Provider, + inputTokens: Int, + outputTokens: Int, + cachedTokens: Int, + totalTokens: Int + ) { + self.inputTokens += inputTokens + self.outputTokens += outputTokens + self.cachedTokens += cachedTokens + self.totalTokens += totalTokens + providerTotals[provider, default: 0] += totalTokens + } +} + +public struct Insight: Identifiable, Hashable, Sendable { + public enum Tone: String, Sendable { + case warning + case positive + case accent + } + + public let id = UUID() + public let tone: Tone + public let title: String + public let message: String +} + +public struct UsageSummary: Sendable { + public let generatedAt: Date + public let records: [UsageRecord] + public let cards: SummaryCards + public let providerTotals: [Provider: TokenBucket] + public let todayProviderTotals: [Provider: TokenBucket] + public let dailyTrend: [DailyPoint] + public let trendPointsByMode: [TrendMode: [DailyPoint]] + public let dailyDetails: [DailyUsageDetail] + public let toolRows: [ToolRow] + public let insights: [Insight] +} diff --git a/NativeTokenLens/Sources/TokenLensCore/SQLiteUsageCache.swift b/NativeTokenLens/Sources/TokenLensCore/SQLiteUsageCache.swift new file mode 100644 index 0000000..f32752e --- /dev/null +++ b/NativeTokenLens/Sources/TokenLensCore/SQLiteUsageCache.swift @@ -0,0 +1,581 @@ +import Foundation +import SQLite3 + +enum SQLiteUsageCacheError: Error { + case openFailed(String) + case prepareFailed(String) + case executeFailed(String) + case bindFailed(String) + case stepFailed(String) +} + +final class SQLiteUsageCache { + private static let transient = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + + private let database: OpaquePointer? + private var calendar: Calendar = { + var calendar = Calendar.current + calendar.firstWeekday = 2 + return calendar + }() + + init(url: URL) throws { + try FileManager.default.createDirectory( + at: url.deletingLastPathComponent(), + withIntermediateDirectories: true + ) + + var database: OpaquePointer? + let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX + guard sqlite3_open_v2(url.path, &database, flags, nil) == SQLITE_OK else { + let message = database.flatMap { String(cString: sqlite3_errmsg($0)) } ?? "无法打开 SQLite" + sqlite3_close(database) + throw SQLiteUsageCacheError.openFailed(message) + } + self.database = database + + try configure() + try migrate() + } + + deinit { + sqlite3_close(database) + } + + func isFileCurrent(path: String, size: Int64, modifiedAt: TimeInterval) throws -> Bool { + let statement = try prepare("SELECT size, modified_at, scanned_offset FROM files WHERE path = ? LIMIT 1") + defer { sqlite3_finalize(statement) } + + try bind(path, to: statement, at: 1) + guard sqlite3_step(statement) == SQLITE_ROW else { + return false + } + + let cachedSize = sqlite3_column_int64(statement, 0) + let cachedModifiedAt = sqlite3_column_double(statement, 1) + let scannedOffset = sqlite3_column_int64(statement, 2) + return cachedSize == size && scannedOffset == size && abs(cachedModifiedAt - modifiedAt) < 0.001 + } + + func fileState(path: String) throws -> CachedFileState? { + let statement = try prepare(""" + SELECT size, modified_at, scanned_offset, record_count + FROM files + WHERE path = ? + LIMIT 1 + """) + defer { sqlite3_finalize(statement) } + + try bind(path, to: statement, at: 1) + guard sqlite3_step(statement) == SQLITE_ROW else { + return nil + } + + return CachedFileState( + size: sqlite3_column_int64(statement, 0), + modifiedAt: sqlite3_column_double(statement, 1), + scannedOffset: sqlite3_column_int64(statement, 2), + recordCount: Int(sqlite3_column_int64(statement, 3)) + ) + } + + func replaceFileRecords( + path: String, + size: Int64, + modifiedAt: TimeInterval, + scan: (_ insert: (UsageRecord) throws -> Void) throws -> Int64 + ) throws { + try execute("BEGIN IMMEDIATE TRANSACTION") + do { + try deleteRecords(path: path) + let insertStatement = try prepare(""" + INSERT INTO records ( + id, provider, source_path, timestamp, session_id, model, + input_tokens, output_tokens, cached_tokens, reasoning_tokens, + tool_tokens, total_tokens, day_start, hour_start, month_start + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """) + defer { sqlite3_finalize(insertStatement) } + + var recordCount = 0 + let scannedOffset = try scan { record in + recordCount += 1 + try self.insert(record, id: "\(path)#\(recordCount)", statement: insertStatement) + } + try upsertFile( + path: path, + size: size, + modifiedAt: modifiedAt, + scannedOffset: scannedOffset, + recordCount: recordCount + ) + try execute("COMMIT") + } catch { + try? execute("ROLLBACK") + throw error + } + } + + func appendFileRecords( + path: String, + size: Int64, + modifiedAt: TimeInterval, + startingRecordCount: Int, + scan: (_ insert: (UsageRecord) throws -> Void) throws -> Int64 + ) throws { + try execute("BEGIN IMMEDIATE TRANSACTION") + do { + let insertStatement = try prepare(""" + INSERT INTO records ( + id, provider, source_path, timestamp, session_id, model, + input_tokens, output_tokens, cached_tokens, reasoning_tokens, + tool_tokens, total_tokens, day_start, hour_start, month_start + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """) + defer { sqlite3_finalize(insertStatement) } + + var appendedCount = 0 + let scannedOffset = try scan { record in + appendedCount += 1 + let nextRecordIndex = startingRecordCount + appendedCount + try self.insert(record, id: "\(path)#\(nextRecordIndex)", statement: insertStatement) + } + try upsertFile( + path: path, + size: size, + modifiedAt: modifiedAt, + scannedOffset: scannedOffset, + recordCount: startingRecordCount + appendedCount + ) + try execute("COMMIT") + } catch { + try? execute("ROLLBACK") + throw error + } + } + + func pruneMissingFiles(currentPaths: Set) throws { + let statement = try prepare("SELECT path FROM files") + defer { sqlite3_finalize(statement) } + + var cachedPaths: [String] = [] + while sqlite3_step(statement) == SQLITE_ROW { + cachedPaths.append(columnText(statement, at: 0)) + } + + for path in cachedPaths where !currentPaths.contains(path) { + try deleteRecords(path: path) + let deleteFile = try prepare("DELETE FROM files WHERE path = ?") + defer { sqlite3_finalize(deleteFile) } + try bind(path, to: deleteFile, at: 1) + try stepDone(deleteFile) + } + } + + func makeSummary(now: Date = Date(), calendar: Calendar = Calendar.current) throws -> UsageSummary { + self.calendar = calendarWithMondayStart(calendar) + + let providerTotals = try providerBuckets() + let todayProviderTotals = try providerBuckets( + whereClause: "timestamp >= ? AND timestamp < ?", + bindings: dayBounds(for: now).bindings + ) + let dailyDetails = try dailyUsageDetails() + let dailyTrend = dailyDetails + .sorted { $0.date < $1.date } + .suffix(30) + .map { detail in + DailyPoint(date: detail.date, values: detail.providerTotals) + } + let trendPointsByMode = try Dictionary(uniqueKeysWithValues: TrendMode.allCases.map { mode in + (mode, try trendPoints(mode: mode, now: now)) + }) + let toolRows = Provider.allCases.compactMap { provider -> ToolRow? in + guard let bucket = providerTotals[provider], bucket.totalTokens > 0 else { return nil } + return ToolRow( + provider: provider, + inputTokens: bucket.inputTokens, + outputTokens: bucket.outputTokens, + cachedTokens: bucket.cachedTokens, + totalTokens: bucket.totalTokens, + trendTokens: dailyTrend.map { $0.values[provider, default: 0] } + ) + } + .sorted { $0.totalTokens > $1.totalTokens } + + let todayTokens = todayProviderTotals.values.reduce(0) { $0 + $1.totalTokens } + let monthTokens = try tokenSum(whereClause: "timestamp >= ? AND timestamp < ?", bindings: monthBounds(for: now).bindings) + let totalTokens = providerTotals.values.reduce(0) { $0 + $1.totalTokens } + let cachedTokens = providerTotals.values.reduce(0) { $0 + $1.cachedTokens } + let cacheHitRate = totalTokens > 0 ? Int((Double(cachedTokens) / Double(totalTokens) * 100).rounded()) : 0 + let cards = SummaryCards( + todayTokens: todayTokens, + monthTokens: monthTokens, + totalTokens: totalTokens, + cacheHitRate: cacheHitRate, + sessionCount: try sessionCount() + ) + + return UsageSummary( + generatedAt: now, + records: [], + cards: cards, + providerTotals: providerTotals, + todayProviderTotals: todayProviderTotals, + dailyTrend: Array(dailyTrend), + trendPointsByMode: trendPointsByMode, + dailyDetails: dailyDetails.sorted { $0.date > $1.date }, + toolRows: toolRows, + insights: UsageSummary.makeInsights( + providerTotals: providerTotals, + todayTokens: todayTokens, + cacheHitRate: cacheHitRate + ) + ) + } + + private func configure() throws { + try execute("PRAGMA journal_mode = WAL") + try execute("PRAGMA synchronous = NORMAL") + try execute("PRAGMA temp_store = MEMORY") + try execute("PRAGMA busy_timeout = 3000") + } + + private func migrate() throws { + if try recordsTableNeedsRebuild() { + try execute("DROP TABLE IF EXISTS records") + try execute("DROP TABLE IF EXISTS files") + } + try execute(""" + CREATE TABLE IF NOT EXISTS files ( + path TEXT PRIMARY KEY, + size INTEGER NOT NULL, + modified_at REAL NOT NULL, + scanned_offset INTEGER NOT NULL DEFAULT 0, + record_count INTEGER NOT NULL DEFAULT 0, + scanned_at REAL NOT NULL + ) + """) + try addColumnIfMissing( + table: "files", + column: "scanned_offset", + definition: "scanned_offset INTEGER NOT NULL DEFAULT 0" + ) + try addColumnIfMissing( + table: "files", + column: "record_count", + definition: "record_count INTEGER NOT NULL DEFAULT 0" + ) + try execute(""" + CREATE TABLE IF NOT EXISTS records ( + id TEXT PRIMARY KEY, + provider TEXT NOT NULL, + source_path TEXT NOT NULL, + timestamp REAL NOT NULL, + session_id TEXT NOT NULL, + model TEXT NOT NULL, + input_tokens INTEGER NOT NULL, + output_tokens INTEGER NOT NULL, + cached_tokens INTEGER NOT NULL, + reasoning_tokens INTEGER NOT NULL, + tool_tokens INTEGER NOT NULL, + total_tokens INTEGER NOT NULL, + day_start REAL NOT NULL, + hour_start REAL NOT NULL, + month_start REAL NOT NULL + ) + """) + try execute("CREATE INDEX IF NOT EXISTS records_provider_idx ON records(provider)") + try execute("CREATE INDEX IF NOT EXISTS records_source_path_idx ON records(source_path)") + try execute("CREATE INDEX IF NOT EXISTS records_timestamp_idx ON records(timestamp)") + try execute("CREATE INDEX IF NOT EXISTS records_day_provider_idx ON records(day_start, provider)") + try execute("CREATE INDEX IF NOT EXISTS records_hour_provider_idx ON records(hour_start, provider)") + try execute("CREATE INDEX IF NOT EXISTS records_month_provider_idx ON records(month_start, provider)") + } + + private func deleteRecords(path: String) throws { + let statement = try prepare("DELETE FROM records WHERE source_path = ?") + defer { sqlite3_finalize(statement) } + try bind(path, to: statement, at: 1) + try stepDone(statement) + } + + private func upsertFile( + path: String, + size: Int64, + modifiedAt: TimeInterval, + scannedOffset: Int64, + recordCount: Int + ) throws { + let statement = try prepare(""" + INSERT INTO files (path, size, modified_at, scanned_offset, record_count, scanned_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(path) DO UPDATE SET + size = excluded.size, + modified_at = excluded.modified_at, + scanned_offset = excluded.scanned_offset, + record_count = excluded.record_count, + scanned_at = excluded.scanned_at + """) + defer { sqlite3_finalize(statement) } + + try bind(path, to: statement, at: 1) + sqlite3_bind_int64(statement, 2, size) + sqlite3_bind_double(statement, 3, modifiedAt) + sqlite3_bind_int64(statement, 4, scannedOffset) + sqlite3_bind_int64(statement, 5, Int64(recordCount)) + sqlite3_bind_double(statement, 6, Date().timeIntervalSince1970) + try stepDone(statement) + } + + private func insert(_ record: UsageRecord, id: String, statement: OpaquePointer?) throws { + sqlite3_reset(statement) + sqlite3_clear_bindings(statement) + + try bind(id, to: statement, at: 1) + try bind(record.provider.rawValue, to: statement, at: 2) + try bind(record.sourcePath, to: statement, at: 3) + sqlite3_bind_double(statement, 4, record.timestamp.timeIntervalSince1970) + try bind(record.sessionID, to: statement, at: 5) + try bind(record.model, to: statement, at: 6) + sqlite3_bind_int64(statement, 7, Int64(record.inputTokens)) + sqlite3_bind_int64(statement, 8, Int64(record.outputTokens)) + sqlite3_bind_int64(statement, 9, Int64(record.cachedTokens)) + sqlite3_bind_int64(statement, 10, Int64(record.reasoningTokens)) + sqlite3_bind_int64(statement, 11, Int64(record.toolTokens)) + sqlite3_bind_int64(statement, 12, Int64(record.totalTokens)) + sqlite3_bind_double(statement, 13, calendar.startOfDay(for: record.timestamp).timeIntervalSince1970) + sqlite3_bind_double(statement, 14, hourStart(for: record.timestamp).timeIntervalSince1970) + sqlite3_bind_double(statement, 15, monthStart(for: record.timestamp).timeIntervalSince1970) + try stepDone(statement) + } + + private func providerBuckets( + whereClause: String? = nil, + bindings: [TimeInterval] = [] + ) throws -> [Provider: TokenBucket] { + var buckets = Dictionary(uniqueKeysWithValues: Provider.allCases.map { + ($0, TokenBucket(provider: $0)) + }) + var sql = """ + SELECT provider, + SUM(input_tokens), SUM(output_tokens), SUM(cached_tokens), + SUM(reasoning_tokens), SUM(tool_tokens), SUM(total_tokens), COUNT(*) + FROM records + """ + if let whereClause { + sql += " WHERE \(whereClause)" + } + sql += " GROUP BY provider" + + let statement = try prepare(sql) + defer { sqlite3_finalize(statement) } + try bindIntervals(bindings, to: statement) + + while sqlite3_step(statement) == SQLITE_ROW { + guard let provider = Provider(rawValue: columnText(statement, at: 0)) else { continue } + var bucket = TokenBucket(provider: provider) + bucket.inputTokens = Int(sqlite3_column_int64(statement, 1)) + bucket.outputTokens = Int(sqlite3_column_int64(statement, 2)) + bucket.cachedTokens = Int(sqlite3_column_int64(statement, 3)) + bucket.reasoningTokens = Int(sqlite3_column_int64(statement, 4)) + bucket.toolTokens = Int(sqlite3_column_int64(statement, 5)) + bucket.totalTokens = Int(sqlite3_column_int64(statement, 6)) + bucket.count = Int(sqlite3_column_int64(statement, 7)) + buckets[provider] = bucket + } + + return buckets + } + + private func dailyUsageDetails() throws -> [DailyUsageDetail] { + let statement = try prepare(""" + SELECT day_start, provider, + SUM(input_tokens), SUM(output_tokens), SUM(cached_tokens), SUM(total_tokens) + FROM records + GROUP BY day_start, provider + ORDER BY day_start DESC + """) + defer { sqlite3_finalize(statement) } + + var details: [Date: DailyUsageDetail] = [:] + while sqlite3_step(statement) == SQLITE_ROW { + guard let provider = Provider(rawValue: columnText(statement, at: 1)) else { continue } + let date = Date(timeIntervalSince1970: sqlite3_column_double(statement, 0)) + if details[date] == nil { + details[date] = DailyUsageDetail(date: date) + } + details[date]?.add( + provider: provider, + inputTokens: Int(sqlite3_column_int64(statement, 2)), + outputTokens: Int(sqlite3_column_int64(statement, 3)), + cachedTokens: Int(sqlite3_column_int64(statement, 4)), + totalTokens: Int(sqlite3_column_int64(statement, 5)) + ) + } + + return details.values.sorted { $0.date > $1.date } + } + + private func trendPoints(mode: TrendMode, now: Date) throws -> [DailyPoint] { + let reference = mode.periodStart(for: now, calendar: calendar) + let start = mode.startDate(reference: reference, calendar: calendar) + let column = mode.periodColumn + let statement = try prepare(""" + SELECT \(column), provider, SUM(total_tokens) + FROM records + WHERE \(column) >= ? AND \(column) <= ? + GROUP BY \(column), provider + """) + defer { sqlite3_finalize(statement) } + sqlite3_bind_double(statement, 1, start.timeIntervalSince1970) + sqlite3_bind_double(statement, 2, reference.timeIntervalSince1970) + + var buckets: [Date: [Provider: Int]] = [:] + while sqlite3_step(statement) == SQLITE_ROW { + guard let provider = Provider(rawValue: columnText(statement, at: 1)) else { continue } + let date = Date(timeIntervalSince1970: sqlite3_column_double(statement, 0)) + buckets[date, default: Provider.zeroTokenMap][provider, default: 0] = Int(sqlite3_column_int64(statement, 2)) + } + + var points: [DailyPoint] = [] + var cursor = start + while cursor <= reference { + points.append(DailyPoint(date: cursor, values: buckets[cursor, default: Provider.zeroTokenMap])) + guard let next = calendar.date(byAdding: mode.calendarComponent, value: 1, to: cursor) else { break } + cursor = next + } + return points + } + + private func tokenSum(whereClause: String, bindings: [TimeInterval]) throws -> Int { + let statement = try prepare("SELECT COALESCE(SUM(total_tokens), 0) FROM records WHERE \(whereClause)") + defer { sqlite3_finalize(statement) } + try bindIntervals(bindings, to: statement) + guard sqlite3_step(statement) == SQLITE_ROW else { return 0 } + return Int(sqlite3_column_int64(statement, 0)) + } + + private func sessionCount() throws -> Int { + let statement = try prepare(""" + SELECT COUNT(DISTINCT CASE WHEN session_id = '' THEN source_path ELSE session_id END) + FROM records + """) + defer { sqlite3_finalize(statement) } + guard sqlite3_step(statement) == SQLITE_ROW else { return 0 } + return Int(sqlite3_column_int64(statement, 0)) + } + + private func recordsTableNeedsRebuild() throws -> Bool { + let columns = try tableColumns("records") + return !columns.isEmpty && !columns.contains("day_start") + } + + private func addColumnIfMissing(table: String, column: String, definition: String) throws { + let columns = try tableColumns(table) + guard !columns.isEmpty, !columns.contains(column) else { return } + try execute("ALTER TABLE \(table) ADD COLUMN \(definition)") + } + + private func tableColumns(_ table: String) throws -> Set { + let statement = try prepare("PRAGMA table_info(\(table))") + defer { sqlite3_finalize(statement) } + + var columns = Set() + while sqlite3_step(statement) == SQLITE_ROW { + columns.insert(columnText(statement, at: 1)) + } + return columns + } + + private func bindIntervals(_ values: [TimeInterval], to statement: OpaquePointer?) throws { + for (index, value) in values.enumerated() { + sqlite3_bind_double(statement, Int32(index + 1), value) + } + } + + private func dayBounds(for date: Date) -> QueryBounds { + let start = calendar.startOfDay(for: date) + let end = calendar.date(byAdding: .day, value: 1, to: start) ?? date + return QueryBounds(start: start, end: end) + } + + private func monthBounds(for date: Date) -> QueryBounds { + let start = monthStart(for: date) + let end = calendar.date(byAdding: .month, value: 1, to: start) ?? date + return QueryBounds(start: start, end: end) + } + + private func hourStart(for date: Date) -> Date { + let components = calendar.dateComponents([.year, .month, .day, .hour], from: date) + return calendar.date(from: components) ?? calendar.startOfDay(for: date) + } + + private func monthStart(for date: Date) -> Date { + let components = calendar.dateComponents([.year, .month], from: date) + return calendar.date(from: components) ?? calendar.startOfDay(for: date) + } + + private func calendarWithMondayStart(_ calendar: Calendar) -> Calendar { + var copy = calendar + copy.firstWeekday = 2 + return copy + } + + private func prepare(_ sql: String) throws -> OpaquePointer? { + var statement: OpaquePointer? + guard sqlite3_prepare_v2(database, sql, -1, &statement, nil) == SQLITE_OK else { + throw SQLiteUsageCacheError.prepareFailed(errorMessage) + } + return statement + } + + private func execute(_ sql: String) throws { + guard sqlite3_exec(database, sql, nil, nil, nil) == SQLITE_OK else { + throw SQLiteUsageCacheError.executeFailed(errorMessage) + } + } + + private func bind(_ text: String, to statement: OpaquePointer?, at index: Int32) throws { + let result = text.withCString { + sqlite3_bind_text(statement, index, $0, -1, Self.transient) + } + guard result == SQLITE_OK else { + throw SQLiteUsageCacheError.bindFailed(errorMessage) + } + } + + private func stepDone(_ statement: OpaquePointer?) throws { + guard sqlite3_step(statement) == SQLITE_DONE else { + throw SQLiteUsageCacheError.stepFailed(errorMessage) + } + } + + private func columnText(_ statement: OpaquePointer?, at index: Int32) -> String { + guard let text = sqlite3_column_text(statement, index) else { + return "" + } + return String(cString: UnsafeRawPointer(text).assumingMemoryBound(to: CChar.self)) + } + + private var errorMessage: String { + database.flatMap { String(cString: sqlite3_errmsg($0)) } ?? "SQLite 操作失败" + } + +} + +struct CachedFileState { + let size: Int64 + let modifiedAt: TimeInterval + let scannedOffset: Int64 + let recordCount: Int +} + +private struct QueryBounds { + let start: Date + let end: Date + + var bindings: [TimeInterval] { + [start.timeIntervalSince1970, end.timeIntervalSince1970] + } +} diff --git a/NativeTokenLens/Sources/TokenLensCore/TokenFormatter.swift b/NativeTokenLens/Sources/TokenLensCore/TokenFormatter.swift new file mode 100644 index 0000000..0db60d9 --- /dev/null +++ b/NativeTokenLens/Sources/TokenLensCore/TokenFormatter.swift @@ -0,0 +1,19 @@ +import Foundation + +public enum TokenFormatter { + public static func format(_ value: Int) -> String { + if value >= 100_000_000 { + return adaptive(Double(value) / 100_000_000) + "亿" + } + if value >= 10_000 { + return adaptive(Double(value) / 10_000) + "万" + } + return String(value) + } + + private static func adaptive(_ value: Double) -> String { + let integerDigits = String(Int(abs(value.rounded(.towardZero)))).count + let digits = integerDigits > 3 ? 1 : 3 + return String(format: "%.\(digits)f", value) + } +} diff --git a/NativeTokenLens/Sources/TokenLensCore/TokenUsageParser.swift b/NativeTokenLens/Sources/TokenLensCore/TokenUsageParser.swift new file mode 100644 index 0000000..31db3a0 --- /dev/null +++ b/NativeTokenLens/Sources/TokenLensCore/TokenUsageParser.swift @@ -0,0 +1,105 @@ +import Foundation + +public enum TokenUsageParser { + public static func parseClaudeLine(_ line: String, sourcePath: String) -> UsageRecord? { + guard let object = parseJSONObject(line) else { return nil } + let message = object["message"] as? [String: Any] + let usage = message?["usage"] as? [String: Any] ?? object["usage"] as? [String: Any] + guard let usage else { return nil } + guard let timestamp = parseDate(object["timestamp"] ?? object["createdAt"] ?? object["date"]) else { return nil } + + return UsageRecord( + provider: .claude, + source: sourceLabel(sourcePath: sourcePath, provider: .claude), + sourcePath: sourcePath, + timestamp: timestamp, + sessionID: stringValue(object["sessionId"]), + model: stringValue(message?["model"]) == "" ? "Claude" : stringValue(message?["model"]), + inputTokens: intValue(usage["input_tokens"]), + outputTokens: intValue(usage["output_tokens"]), + cachedTokens: intValue(usage["cache_read_input_tokens"]) + intValue(usage["cache_creation_input_tokens"]), + reasoningTokens: 0, + toolTokens: 0 + ) + } + + public static func parseCodexLine(_ line: String, sourcePath: String) -> UsageRecord? { + guard let object = parseJSONObject(line), + let payload = object["payload"] as? [String: Any], + let info = payload["info"] as? [String: Any], + let usage = info["last_token_usage"] as? [String: Any], + let timestamp = parseDate(object["timestamp"]) + else { return nil } + + return UsageRecord( + provider: .codex, + source: sourceLabel(sourcePath: sourcePath, provider: .codex), + sourcePath: sourcePath, + timestamp: timestamp, + sessionID: stringValue(payload["id"] ?? payload["thread_id"]), + model: stringValue(payload["model"]) == "" ? "Codex" : stringValue(payload["model"]), + inputTokens: intValue(usage["input_tokens"]), + outputTokens: intValue(usage["output_tokens"]), + cachedTokens: intValue(usage["cached_input_tokens"]), + reasoningTokens: intValue(usage["reasoning_output_tokens"]), + toolTokens: 0 + ) + } + + public static func parseGeminiLine(_ line: String, sourcePath: String) -> UsageRecord? { + guard let object = parseJSONObject(line), + let tokens = object["tokens"] as? [String: Any], + let timestamp = parseDate(object["timestamp"]) + else { return nil } + + return UsageRecord( + provider: .gemini, + source: sourceLabel(sourcePath: sourcePath, provider: .gemini), + sourcePath: sourcePath, + timestamp: timestamp, + sessionID: stringValue(object["id"]), + model: stringValue(object["model"]) == "" ? "Gemini" : stringValue(object["model"]), + inputTokens: intValue(tokens["input"]), + outputTokens: intValue(tokens["output"]), + cachedTokens: intValue(tokens["cached"]), + reasoningTokens: intValue(tokens["thoughts"]), + toolTokens: intValue(tokens["tool"]), + explicitTotal: intValue(tokens["total"]) == 0 ? nil : intValue(tokens["total"]) + ) + } + + static func parseDate(_ value: Any?) -> Date? { + guard let text = value as? String else { return nil } + let isoFormatter = ISO8601DateFormatter() + isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let fallbackISOFormatter = ISO8601DateFormatter() + fallbackISOFormatter.formatOptions = [.withInternetDateTime] + return isoFormatter.date(from: text) ?? fallbackISOFormatter.date(from: text) + } + + private static func parseJSONObject(_ line: String) -> [String: Any]? { + guard let data = line.data(using: .utf8) else { return nil } + return (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] + } + + private static func stringValue(_ value: Any?) -> String { + value as? String ?? "" + } + + private static func intValue(_ value: Any?) -> Int { + if let int = value as? Int { return int } + if let double = value as? Double { return Int(double) } + if let string = value as? String { return Int(string) ?? 0 } + return 0 + } + + private static func sourceLabel(sourcePath: String, provider: Provider) -> String { + switch provider { + case .claude: return "projects/*.jsonl" + case .codex: return "sessions/*.jsonl" + case .gemini: return "tmp/chats/*.jsonl" + case .hermes: return "state.db/sessions" + case .opencode: return "opencode.db/session" + } + } +} diff --git a/NativeTokenLens/Sources/TokenLensCore/TokenUsageScanner.swift b/NativeTokenLens/Sources/TokenLensCore/TokenUsageScanner.swift new file mode 100644 index 0000000..2f89232 --- /dev/null +++ b/NativeTokenLens/Sources/TokenLensCore/TokenUsageScanner.swift @@ -0,0 +1,525 @@ +import Foundation +import SQLite3 + +public actor TokenUsageScanner { + public let homeDirectory: URL + public let cacheURL: URL + private let chunkSize = 256 * 1024 + private var lastSignature: FileSignature? + private var lastSummary: UsageSummary? + + public init( + homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser, + cacheURL: URL? = nil + ) { + self.homeDirectory = homeDirectory + self.cacheURL = cacheURL ?? homeDirectory + .appending(path: "Library/Application Support/TokenLens") + .appending(path: "usage-cache.sqlite") + } + + public func scan(now: Date = Date()) async -> UsageSummary { + defer { MemoryPressure.relieve() } + + if let summary = scanWithCache(now: now) { + return summary + } + + var accumulator = UsageSummaryAccumulator(now: now) + scanClaude(into: &accumulator) + scanCodex(into: &accumulator) + scanGemini(into: &accumulator) + scanHermes(into: &accumulator) + scanOpenCode(into: &accumulator) + return accumulator.makeSummary() + } + + private func scanWithCache(now: Date) -> UsageSummary? { + do { + let snapshots = try logFileSnapshots() + let signature = FileSignature(snapshots: snapshots) + if signature == lastSignature, + let lastSummary, + isSameDisplayWindow(lastSummary.generatedAt, now) { + let updated = lastSummary.updatingGeneratedAt(now) + self.lastSummary = updated + return updated + } + + let cache = try SQLiteUsageCache(url: cacheURL) + let currentPaths = try refreshCache(cache, snapshots: snapshots) + try cache.pruneMissingFiles(currentPaths: currentPaths) + let summary = try cache.makeSummary(now: now) + lastSignature = signature + lastSummary = summary + return summary + } catch { + return nil + } + } + + private func refreshCache(_ cache: SQLiteUsageCache, snapshots: [LogFileSnapshot]) throws -> Set { + var currentPaths = Set() + for snapshot in snapshots { + currentPaths.insert(snapshot.path) + let state = try cache.fileState(path: snapshot.path) + let isCurrent = try cache.isFileCurrent( + path: snapshot.path, + size: snapshot.size, + modifiedAt: snapshot.modifiedAt + ) + if isCurrent && (snapshot.scanner.supportsZeroRecordCache || (state?.recordCount ?? 0) > 0) { + continue + } + + if let state, + snapshot.size > state.size, + state.size == state.scannedOffset, + state.scannedOffset > 0, + snapshot.scanner.supportsIncrementalScan { + try cache.appendFileRecords( + path: snapshot.path, + size: snapshot.size, + modifiedAt: snapshot.modifiedAt, + startingRecordCount: state.recordCount + ) { insert in + try scanFile( + snapshot.url, + startingAt: UInt64(state.scannedOffset), + scanner: snapshot.scanner, + consume: insert + ).scannedOffset + } + } else { + try cache.replaceFileRecords( + path: snapshot.path, + size: snapshot.size, + modifiedAt: snapshot.modifiedAt + ) { insert in + try scanFile(snapshot.url, scanner: snapshot.scanner, consume: insert).scannedOffset + } + } + } + return currentPaths + } + + private func scanClaude(into accumulator: inout UsageSummaryAccumulator) { + scanJSONLFiles( + root: homeDirectory.appending(path: ".claude/projects"), + include: { $0.pathExtension == "jsonl" }, + parse: TokenUsageParser.parseClaudeLine, + into: &accumulator + ) + } + + private func scanCodex(into accumulator: inout UsageSummaryAccumulator) { + scanJSONLFiles( + root: homeDirectory.appending(path: ".codex/sessions"), + include: { $0.pathExtension == "jsonl" }, + parse: TokenUsageParser.parseCodexLine, + into: &accumulator + ) + } + + private func scanGemini(into accumulator: inout UsageSummaryAccumulator) { + scanJSONLFiles( + root: homeDirectory.appending(path: ".gemini"), + include: { url in + url.pathExtension == "jsonl" && url.pathComponents.contains("chats") + }, + parse: TokenUsageParser.parseGeminiLine, + into: &accumulator + ) + } + + private func scanHermes(into accumulator: inout UsageSummaryAccumulator) { + for file in files(root: homeDirectory.appending(path: ".hermes"), include: { $0.lastPathComponent == "state.db" }) { + _ = try? scanHermesStateDB(file) { record in + accumulator.add(record) + } + } + } + + private func scanOpenCode(into accumulator: inout UsageSummaryAccumulator) { + for file in files(root: homeDirectory.appending(path: ".local/share/opencode"), include: { $0.lastPathComponent == "opencode.db" }) { + _ = try? scanOpenCodeDB(file) { record in + accumulator.add(record) + } + } + } + + private func scanJSONLFiles( + root: URL, + include: (URL) -> Bool, + parse: (String, String) -> UsageRecord?, + into accumulator: inout UsageSummaryAccumulator + ) { + for file in files(root: root, include: include) { + _ = try? scanJSONLFile(file, parse: parse) { record in + accumulator.add(record) + } + } + } + + private func scanFile( + _ file: URL, + startingAt offset: UInt64 = 0, + scanner: SourceScanner, + consume: (UsageRecord) throws -> Void + ) throws -> ScanProgress { + switch scanner { + case let .jsonl(parse): + return try scanJSONLFile(file, startingAt: offset, parse: parse, consume: consume) + case .hermesStateDB: + return try scanHermesStateDB(file, consume: consume) + case .openCodeDB: + return try scanOpenCodeDB(file, consume: consume) + } + } + + private func scanJSONLFile( + _ file: URL, + startingAt offset: UInt64 = 0, + parse: (String, String) -> UsageRecord?, + consume: (UsageRecord) throws -> Void + ) throws -> ScanProgress { + guard let handle = try? FileHandle(forReadingFrom: file) else { + return ScanProgress(scannedOffset: Int64(offset), recordCount: 0) + } + defer { try? handle.close() } + try handle.seek(toOffset: offset) + + var pendingLine = Data() + var scannedOffset = Int64(offset) + var recordCount = 0 + + while true { + let chunk = (try? handle.read(upToCount: chunkSize)) ?? Data() + if chunk.isEmpty { break } + + var lineStart = chunk.startIndex + for index in chunk.indices where chunk[index] == 0x0A { + let lineData = chunk[lineStart..( + from lineData: LineData, + sourcePath: String, + parse: (String, String) -> UsageRecord?, + consume: (UsageRecord) throws -> Void + ) throws -> Bool where LineData.Element == UInt8 { + guard !lineData.isEmpty, + let line = String(bytes: lineData, encoding: .utf8), + let record = parse(line, sourcePath) + else { + return false + } + try consume(record) + return true + } + + private func isCompleteJSONLine(_ lineData: Data) -> Bool { + guard !lineData.isEmpty else { return true } + return (try? JSONSerialization.jsonObject(with: lineData)) != nil + } + + private func scanHermesStateDB( + _ file: URL, + consume: (UsageRecord) throws -> Void + ) throws -> ScanProgress { + let sql = """ + SELECT id, started_at, COALESCE(model, ''), + input_tokens, output_tokens, cache_read_tokens, + cache_write_tokens, reasoning_tokens + FROM sessions + WHERE input_tokens + output_tokens + cache_read_tokens + cache_write_tokens + reasoning_tokens > 0 + """ + return try scanSQLiteRows(file, sql: sql) { statement in + UsageRecord( + provider: .hermes, + source: "state.db/sessions", + sourcePath: file.path, + timestamp: Date(timeIntervalSince1970: sqlite3_column_double(statement, 1)), + sessionID: textColumn(statement, 0), + model: textColumn(statement, 2).isEmpty ? "Hermes" : textColumn(statement, 2), + inputTokens: intColumn(statement, 3), + outputTokens: intColumn(statement, 4), + cachedTokens: intColumn(statement, 5) + intColumn(statement, 6), + reasoningTokens: intColumn(statement, 7), + toolTokens: 0 + ) + } consume: { + try consume($0) + } + } + + private func scanOpenCodeDB( + _ file: URL, + consume: (UsageRecord) throws -> Void + ) throws -> ScanProgress { + let sql = """ + SELECT id, COALESCE(NULLIF(time_updated, 0), time_created), COALESCE(model, ''), + tokens_input, tokens_output, tokens_reasoning, + tokens_cache_read, tokens_cache_write + FROM session + WHERE tokens_input + tokens_output + tokens_reasoning + tokens_cache_read + tokens_cache_write > 0 + """ + return try scanSQLiteRows(file, sql: sql) { statement in + UsageRecord( + provider: .opencode, + source: "opencode.db/session", + sourcePath: file.path, + timestamp: Date(timeIntervalSince1970: sqlite3_column_double(statement, 1) / 1000), + sessionID: textColumn(statement, 0), + model: openCodeModelName(textColumn(statement, 2)), + inputTokens: intColumn(statement, 3), + outputTokens: intColumn(statement, 4), + cachedTokens: intColumn(statement, 6) + intColumn(statement, 7), + reasoningTokens: intColumn(statement, 5), + toolTokens: 0 + ) + } consume: { + try consume($0) + } + } + + private func scanSQLiteRows( + _ file: URL, + sql: String, + makeRecord: (OpaquePointer?) -> UsageRecord, + consume: (UsageRecord) throws -> Void + ) throws -> ScanProgress { + var database: OpaquePointer? + let flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX + guard sqlite3_open_v2(file.path, &database, flags, nil) == SQLITE_OK else { + sqlite3_close(database) + return ScanProgress(scannedOffset: fileSize(file), recordCount: 0) + } + defer { sqlite3_close(database) } + sqlite3_busy_timeout(database, 1000) + + var statement: OpaquePointer? + guard sqlite3_prepare_v2(database, sql, -1, &statement, nil) == SQLITE_OK else { + sqlite3_finalize(statement) + return ScanProgress(scannedOffset: fileSize(file), recordCount: 0) + } + defer { sqlite3_finalize(statement) } + + var recordCount = 0 + while sqlite3_step(statement) == SQLITE_ROW { + try consume(makeRecord(statement)) + recordCount += 1 + } + return ScanProgress(scannedOffset: fileSize(file), recordCount: recordCount) + } + + private func fileSize(_ file: URL) -> Int64 { + ((try? file.resourceValues(forKeys: [.fileSizeKey]).fileSize).flatMap { Int64($0) }) ?? 0 + } + + private func textColumn(_ statement: OpaquePointer?, _ index: Int32) -> String { + guard let text = sqlite3_column_text(statement, index) else { return "" } + return String(cString: UnsafeRawPointer(text).assumingMemoryBound(to: CChar.self)) + } + + private func intColumn(_ statement: OpaquePointer?, _ index: Int32) -> Int { + Int(sqlite3_column_int64(statement, index)) + } + + private func openCodeModelName(_ rawModel: String) -> String { + guard !rawModel.isEmpty, + let data = rawModel.data(using: .utf8), + let object = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] + else { + return rawModel.isEmpty ? "OpenCode" : rawModel + } + let provider = object["providerID"] as? String + let model = object["id"] as? String + var parts: [String] = [] + if let provider, !provider.isEmpty { + parts.append(provider) + } + if let model, !model.isEmpty { + parts.append(model) + } + return parts.isEmpty ? "OpenCode" : parts.joined(separator: "/") + } + + private func files(root: URL, include: (URL) -> Bool) -> [URL] { + guard let enumerator = FileManager.default.enumerator( + at: root, + includingPropertiesForKeys: [.isRegularFileKey], + options: [.skipsHiddenFiles] + ) else { + return [] + } + + var result: [URL] = [] + for case let file as URL in enumerator where include(file) { + result.append(file) + } + return result + } + + private var logSources: [LogSource] { + [ + LogSource( + root: homeDirectory.appending(path: ".claude/projects"), + include: { $0.pathExtension == "jsonl" }, + scanner: .jsonl(TokenUsageParser.parseClaudeLine) + ), + LogSource( + root: homeDirectory.appending(path: ".codex/sessions"), + include: { $0.pathExtension == "jsonl" }, + scanner: .jsonl(TokenUsageParser.parseCodexLine) + ), + LogSource( + root: homeDirectory.appending(path: ".gemini"), + include: { url in + url.pathExtension == "jsonl" && url.pathComponents.contains("chats") + }, + scanner: .jsonl(TokenUsageParser.parseGeminiLine) + ), + LogSource( + root: homeDirectory.appending(path: ".hermes"), + include: { + $0.lastPathComponent == "state.db" && + $0.deletingLastPathComponent().lastPathComponent == ".hermes" + }, + scanner: .hermesStateDB + ), + LogSource( + root: homeDirectory.appending(path: ".local/share/opencode"), + include: { + $0.lastPathComponent == "opencode.db" && + $0.deletingLastPathComponent().lastPathComponent == "opencode" + }, + scanner: .openCodeDB + ) + ] + } + + private func logFileSnapshots() throws -> [LogFileSnapshot] { + var snapshots: [LogFileSnapshot] = [] + for source in logSources { + for file in files(root: source.root, include: source.include) { + let metadata = try fileMetadata(file) + snapshots.append(LogFileSnapshot( + url: file, + path: file.path, + size: metadata.size, + modifiedAt: metadata.modifiedAt, + scanner: source.scanner + )) + } + } + return snapshots.sorted { $0.path < $1.path } + } + + private func fileMetadata(_ file: URL) throws -> FileMetadata { + let values = try file.resourceValues(forKeys: [.fileSizeKey, .contentModificationDateKey]) + return FileMetadata( + size: Int64(values.fileSize ?? 0), + modifiedAt: values.contentModificationDate?.timeIntervalSince1970 ?? 0 + ) + } + + private func isSameDisplayWindow(_ lhs: Date, _ rhs: Date) -> Bool { + var calendar = Calendar.current + calendar.firstWeekday = 2 + return calendar.isDate(lhs, equalTo: rhs, toGranularity: .hour) + } +} + +private struct LogSource { + let root: URL + let include: (URL) -> Bool + let scanner: SourceScanner +} + +private enum SourceScanner { + case jsonl((String, String) -> UsageRecord?) + case hermesStateDB + case openCodeDB + + var supportsIncrementalScan: Bool { + if case .jsonl = self { return true } + return false + } + + var supportsZeroRecordCache: Bool { + if case .jsonl = self { return true } + return false + } +} + +private struct ScanProgress { + let scannedOffset: Int64 + let recordCount: Int +} + +private struct FileMetadata { + let size: Int64 + let modifiedAt: TimeInterval +} + +private struct LogFileSnapshot { + let url: URL + let path: String + let size: Int64 + let modifiedAt: TimeInterval + let scanner: SourceScanner +} + +private struct FileSignature: Equatable { + let entries: [Entry] + + init(snapshots: [LogFileSnapshot]) { + entries = snapshots.map { + Entry(path: $0.path, size: $0.size, modifiedAt: $0.modifiedAt) + } + } + + struct Entry: Equatable { + let path: String + let size: Int64 + let modifiedAt: TimeInterval + } +} diff --git a/NativeTokenLens/Sources/TokenLensCore/UsageSummary.swift b/NativeTokenLens/Sources/TokenLensCore/UsageSummary.swift new file mode 100644 index 0000000..988faf3 --- /dev/null +++ b/NativeTokenLens/Sources/TokenLensCore/UsageSummary.swift @@ -0,0 +1,330 @@ +import Foundation + +public struct UsageSummaryAccumulator { + private let now: Date + private var calendar: Calendar + private let today: Date + private let month: DateComponents + private let trendWindows: [TrendMode: TrendWindow] + + private var providerTotals: [Provider: TokenBucket] + private var todayProviderTotals: [Provider: TokenBucket] + private var dailyBuckets: [Date: [Provider: Int]] = [:] + private var dailyDetails: [Date: DailyUsageDetail] = [:] + private var trendBuckets: [TrendMode: [Date: [Provider: Int]]] = [:] + private var todayTokens = 0 + private var monthTokens = 0 + private var totalTokens = 0 + private var cachedTokens = 0 + private var sessions = Set() + + public init( + now: Date = Date(), + calendar baseCalendar: Calendar = Calendar.current + ) { + self.now = now + var calendar = baseCalendar + calendar.firstWeekday = 2 + self.calendar = calendar + today = calendar.startOfDay(for: now) + month = calendar.dateComponents([.year, .month], from: now) + providerTotals = Dictionary(uniqueKeysWithValues: Provider.allCases.map { + ($0, TokenBucket(provider: $0)) + }) + todayProviderTotals = Dictionary(uniqueKeysWithValues: Provider.allCases.map { + ($0, TokenBucket(provider: $0)) + }) + trendWindows = Dictionary(uniqueKeysWithValues: TrendMode.allCases.map { mode in + let reference = mode.periodStart(for: now, calendar: calendar) + return (mode, TrendWindow( + reference: reference, + start: mode.startDate(reference: reference, calendar: calendar), + component: mode.calendarComponent + )) + }) + } + + public mutating func add(_ record: UsageRecord) { + add( + provider: record.provider, + sourcePath: record.sourcePath, + timestamp: record.timestamp, + sessionID: record.sessionID, + inputTokens: record.inputTokens, + outputTokens: record.outputTokens, + cachedTokens: record.cachedTokens, + reasoningTokens: record.reasoningTokens, + toolTokens: record.toolTokens, + totalTokens: record.totalTokens + ) + } + + public mutating func add( + provider: Provider, + sourcePath: String, + timestamp: Date, + sessionID: String, + inputTokens: Int, + outputTokens: Int, + cachedTokens: Int, + reasoningTokens: Int, + toolTokens: Int, + totalTokens: Int + ) { + self.totalTokens += totalTokens + self.cachedTokens += cachedTokens + sessions.insert(sessionID.isEmpty ? sourcePath : sessionID) + providerTotals[provider, default: TokenBucket(provider: provider)].add( + inputTokens: inputTokens, + outputTokens: outputTokens, + cachedTokens: cachedTokens, + reasoningTokens: reasoningTokens, + toolTokens: toolTokens, + totalTokens: totalTokens + ) + + let recordDay = calendar.startOfDay(for: timestamp) + dailyBuckets[recordDay, default: Provider.zeroTokenMap][provider, default: 0] += totalTokens + if dailyDetails[recordDay] == nil { + dailyDetails[recordDay] = DailyUsageDetail(date: recordDay) + } + dailyDetails[recordDay]?.add( + provider: provider, + inputTokens: inputTokens, + outputTokens: outputTokens, + cachedTokens: cachedTokens, + totalTokens: totalTokens + ) + + if recordDay == today { + todayTokens += totalTokens + todayProviderTotals[provider, default: TokenBucket(provider: provider)].add( + inputTokens: inputTokens, + outputTokens: outputTokens, + cachedTokens: cachedTokens, + reasoningTokens: reasoningTokens, + toolTokens: toolTokens, + totalTokens: totalTokens + ) + } + + let recordMonth = calendar.dateComponents([.year, .month], from: timestamp) + if recordMonth.year == month.year && recordMonth.month == month.month { + monthTokens += totalTokens + } + + for mode in TrendMode.allCases { + guard let window = trendWindows[mode] else { continue } + let period = mode.periodStart(for: timestamp, calendar: calendar) + guard period >= window.start && period <= window.reference else { continue } + trendBuckets[mode, default: [:]][period, default: Provider.zeroTokenMap][provider, default: 0] += totalTokens + } + } + + public func makeSummary() -> UsageSummary { + let dailyTrend = dailyBuckets + .sorted { $0.key < $1.key } + .suffix(30) + .map { DailyPoint(date: $0.key, values: $0.value) } + + let sortedDailyDetails = dailyDetails.values.sorted { $0.date > $1.date } + let trendPointsByMode = Dictionary(uniqueKeysWithValues: TrendMode.allCases.map { mode in + (mode, makeTrendPoints(mode: mode)) + }) + + let toolRows = Provider.allCases.compactMap { provider -> ToolRow? in + guard let bucket = providerTotals[provider], bucket.totalTokens > 0 else { return nil } + let trendTokens = dailyTrend.map { $0.values[provider, default: 0] } + return ToolRow( + provider: provider, + inputTokens: bucket.inputTokens, + outputTokens: bucket.outputTokens, + cachedTokens: bucket.cachedTokens, + totalTokens: bucket.totalTokens, + trendTokens: trendTokens + ) + } + .sorted { $0.totalTokens > $1.totalTokens } + + let cacheHitRate = totalTokens > 0 ? Int((Double(cachedTokens) / Double(totalTokens) * 100).rounded()) : 0 + let cards = SummaryCards( + todayTokens: todayTokens, + monthTokens: monthTokens, + totalTokens: totalTokens, + cacheHitRate: cacheHitRate, + sessionCount: sessions.count + ) + + return UsageSummary( + generatedAt: now, + records: [], + cards: cards, + providerTotals: providerTotals, + todayProviderTotals: todayProviderTotals, + dailyTrend: Array(dailyTrend), + trendPointsByMode: trendPointsByMode, + dailyDetails: sortedDailyDetails, + toolRows: toolRows, + insights: UsageSummary.makeInsights( + providerTotals: providerTotals, + todayTokens: todayTokens, + cacheHitRate: cacheHitRate + ) + ) + } + + private func makeTrendPoints(mode: TrendMode) -> [DailyPoint] { + guard let window = trendWindows[mode] else { return [] } + let buckets = trendBuckets[mode, default: [:]] + var points: [DailyPoint] = [] + var cursor = window.start + while cursor <= window.reference { + points.append(DailyPoint(date: cursor, values: buckets[cursor, default: Provider.zeroTokenMap])) + guard let next = calendar.date(byAdding: window.component, value: 1, to: cursor) else { break } + cursor = next + } + return points + } +} + +private struct TrendWindow { + let reference: Date + let start: Date + let component: Calendar.Component +} + +public extension UsageSummary { + static func make( + records: [UsageRecord], + now: Date = Date(), + calendar baseCalendar: Calendar = Calendar.current + ) -> UsageSummary { + var accumulator = UsageSummaryAccumulator(now: now, calendar: baseCalendar) + for record in records { + accumulator.add(record) + } + return accumulator.makeSummary() + } + + static func makeInsights( + providerTotals: [Provider: TokenBucket], + todayTokens: Int, + cacheHitRate: Int + ) -> [Insight] { + let top = providerTotals.values + .filter { $0.totalTokens > 0 } + .sorted { $0.totalTokens > $1.totalTokens } + .first + + var insights: [Insight] = [] + if let top, let provider = top.provider { + insights.append(Insight( + tone: .warning, + title: "\(provider.rawValue) 是当前主要用量来源", + message: "已从本地日志统计 \(TokenFormatter.format(top.totalTokens)) Tokens。" + )) + } + insights.append(Insight( + tone: .positive, + title: "检测到缓存 Tokens", + message: "当前 \(cacheHitRate)% 的统计用量来自缓存上下文。" + )) + insights.append(Insight( + tone: .accent, + title: "今日用量快照已更新", + message: "当前本地日期已统计 \(TokenFormatter.format(todayTokens)) Tokens。" + )) + return insights + } + + func trendPoints( + mode: TrendMode, + calendar baseCalendar: Calendar = Calendar.current + ) -> [DailyPoint] { + if let points = trendPointsByMode[mode] { + return points + } + return [] + } + + func dailyUsageDetails(calendar baseCalendar: Calendar = Calendar.current) -> [DailyUsageDetail] { + dailyDetails + } + + func updatingGeneratedAt(_ date: Date) -> UsageSummary { + UsageSummary( + generatedAt: date, + records: records, + cards: cards, + providerTotals: providerTotals, + todayProviderTotals: todayProviderTotals, + dailyTrend: dailyTrend, + trendPointsByMode: trendPointsByMode, + dailyDetails: dailyDetails, + toolRows: toolRows, + insights: insights + ) + } +} + +extension Provider { + static var zeroTokenMap: [Provider: Int] { + Dictionary(uniqueKeysWithValues: Provider.allCases.map { ($0, 0) }) + } +} + +extension TrendMode { + var calendarComponent: Calendar.Component { + switch self { + case .daily: + return .hour + case .weekly: + return .day + case .monthly: + return .day + case .yearly: + return .month + } + } + + var periodColumn: String { + switch self { + case .daily: + return "hour_start" + case .weekly, .monthly: + return "day_start" + case .yearly: + return "month_start" + } + } + + func periodStart(for date: Date, calendar: Calendar) -> Date { + switch self { + case .daily: + let components = calendar.dateComponents([.year, .month, .day, .hour], from: date) + return calendar.date(from: components) ?? calendar.startOfDay(for: date) + case .weekly: + return calendar.startOfDay(for: date) + case .monthly: + return calendar.startOfDay(for: date) + case .yearly: + let components = calendar.dateComponents([.year, .month], from: date) + return calendar.date(from: components) ?? calendar.startOfDay(for: date) + } + } + + func startDate(reference: Date, calendar: Calendar) -> Date { + switch self { + case .daily: + return calendar.startOfDay(for: reference) + case .weekly: + return calendar.dateInterval(of: .weekOfYear, for: reference)?.start ?? calendar.startOfDay(for: reference) + case .monthly: + let components = calendar.dateComponents([.year, .month], from: reference) + return calendar.date(from: components) ?? calendar.startOfDay(for: reference) + case .yearly: + let components = calendar.dateComponents([.year], from: reference) + return calendar.date(from: components) ?? calendar.startOfDay(for: reference) + } + } +} diff --git a/NativeTokenLens/Tests/TokenLensCoreTests/TokenUsageTests.swift b/NativeTokenLens/Tests/TokenLensCoreTests/TokenUsageTests.swift new file mode 100644 index 0000000..7418a38 --- /dev/null +++ b/NativeTokenLens/Tests/TokenLensCoreTests/TokenUsageTests.swift @@ -0,0 +1,312 @@ +import Testing +import Foundation +import SQLite3 +@testable import TokenLensCore + +@Test func formatTokensUsesChineseMagnitude() { + #expect(TokenFormatter.format(9_999) == "9999") + #expect(TokenFormatter.format(10_000) == "1.000万") + #expect(TokenFormatter.format(71_400) == "7.140万") + #expect(TokenFormatter.format(6_600_000) == "660.000万") + #expect(TokenFormatter.format(12_345_678) == "1234.6万") + #expect(TokenFormatter.format(196_700_000) == "1.967亿") + #expect(TokenFormatter.format(1_020_000_000) == "10.200亿") + #expect(TokenFormatter.format(100_000_000_000) == "1000.0亿") +} + +@Test func parseClaudeUsageLineSkipsConversationContent() throws { + let line = """ + {"timestamp":"2026-06-11T02:20:00.000Z","sessionId":"s1","cwd":"/tmp","message":{"model":"claude-3-5-sonnet","content":"private text","usage":{"input_tokens":120,"output_tokens":30,"cache_read_input_tokens":50,"cache_creation_input_tokens":10}}} + """ + + let record = try #require(TokenUsageParser.parseClaudeLine(line, sourcePath: "/tmp/a.jsonl")) + + #expect(record.provider == .claude) + #expect(record.model == "claude-3-5-sonnet") + #expect(record.inputTokens == 120) + #expect(record.outputTokens == 30) + #expect(record.cachedTokens == 60) + #expect(record.totalTokens == 210) +} + +@Test func parseCodexUsesLastTokenUsageOnly() throws { + let line = """ + {"timestamp":"2026-06-11T03:10:00.000Z","type":"event_msg","payload":{"info":{"total_token_usage":{"input_tokens":1000,"output_tokens":300,"cached_input_tokens":200,"reasoning_output_tokens":40,"total_tokens":1300},"last_token_usage":{"input_tokens":100,"output_tokens":30,"cached_input_tokens":20,"reasoning_output_tokens":4,"total_tokens":130}}}} + """ + + let record = try #require(TokenUsageParser.parseCodexLine(line, sourcePath: "/tmp/rollout.jsonl")) + + #expect(record.provider == .codex) + #expect(record.inputTokens == 100) + #expect(record.outputTokens == 30) + #expect(record.cachedTokens == 20) + #expect(record.reasoningTokens == 4) + #expect(record.totalTokens == 154) +} + +@Test func parseGeminiTokensLine() throws { + let line = """ + {"timestamp":"2026-06-11T04:00:00.000Z","type":"response","model":"gemini-2.5-pro","tokens":{"input":70,"output":20,"cached":5,"thoughts":3,"tool":2,"total":100}} + """ + + let record = try #require(TokenUsageParser.parseGeminiLine(line, sourcePath: "/tmp/session.jsonl")) + + #expect(record.provider == .gemini) + #expect(record.model == "gemini-2.5-pro") + #expect(record.inputTokens == 70) + #expect(record.outputTokens == 20) + #expect(record.cachedTokens == 5) + #expect(record.reasoningTokens == 3) + #expect(record.toolTokens == 2) + #expect(record.totalTokens == 100) +} + +@Test func summaryAggregatesTodayMonthProvidersAndSources() { + let calendar = Calendar(identifier: .gregorian) + let now = ISO8601DateFormatter().date(from: "2026-06-11T04:00:00Z")! + let records = [ + usage(.claude, "2026-06-11T01:00:00Z", 100, 20, 10), + usage(.codex, "2026-06-10T01:00:00Z", 80, 20, 0), + usage(.gemini, "2026-05-31T01:00:00Z", 50, 10, 0) + ] + + let summary = UsageSummary.make(records: records, now: now, calendar: calendar) + + #expect(summary.cards.todayTokens == 130) + #expect(summary.cards.monthTokens == 230) + #expect(summary.cards.totalTokens == 290) + #expect(summary.providerTotals[.claude]?.totalTokens == 130) + #expect(summary.providerTotals[.codex]?.totalTokens == 100) + #expect(summary.providerTotals[.gemini]?.totalTokens == 60) + #expect(summary.todayProviderTotals[.claude]?.totalTokens == 130) + #expect(summary.todayProviderTotals[.codex]?.totalTokens == 0) + #expect(summary.todayProviderTotals[.gemini]?.totalTokens == 0) + #expect(summary.toolRows.count == 3) +} + +@Test func toolRowsKeepProviderSpecificTrends() { + let calendar = Calendar(identifier: .gregorian) + let now = ISO8601DateFormatter().date(from: "2026-06-11T04:00:00Z")! + let records = [ + usage(.claude, "2026-06-09T01:00:00Z", 10, 0, 0), + usage(.claude, "2026-06-11T01:00:00Z", 30, 0, 0), + usage(.codex, "2026-06-10T01:00:00Z", 20, 0, 0), + usage(.gemini, "2026-06-11T02:00:00Z", 5, 0, 0) + ] + + let summary = UsageSummary.make(records: records, now: now, calendar: calendar) + let trends = Dictionary(uniqueKeysWithValues: summary.toolRows.map { ($0.provider, $0.trendTokens) }) + + #expect(trends[.claude] == [10, 0, 30]) + #expect(trends[.codex] == [0, 20, 0]) + #expect(trends[.gemini] == [0, 0, 5]) +} + +@Test func trendPointsRegroupBySelectedMode() { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + calendar.firstWeekday = 2 + let now = ISO8601DateFormatter().date(from: "2026-06-11T12:30:00Z")! + let records = [ + usage(.claude, "2026-06-11T01:00:00Z", 10, 0, 0), + usage(.codex, "2026-06-11T10:00:00Z", 20, 0, 0), + usage(.claude, "2026-06-11T01:00:00Z", 30, 0, 0), + usage(.gemini, "2026-05-11T01:00:00Z", 5, 0, 0), + usage(.codex, "2026-04-01T01:00:00Z", 7, 0, 0) + ] + let summary = UsageSummary.make(records: records, now: now, calendar: calendar) + + let daily = summary.trendPoints(mode: .daily, calendar: calendar) + #expect(daily.count == 13) + #expect(daily[1].values[.claude] == 40) + #expect(daily[10].values[.codex] == 20) + + let weekly = summary.trendPoints(mode: .weekly, calendar: calendar) + #expect(weekly.count == 4) + #expect(weekly.last?.values[.claude] == 40) + #expect(weekly.last?.values[.codex] == 20) + + let monthly = summary.trendPoints(mode: .monthly, calendar: calendar) + #expect(monthly.count == 11) + #expect(monthly.last?.values[.claude] == 40) + #expect(monthly.last?.values[.codex] == 20) + + let yearly = summary.trendPoints(mode: .yearly, calendar: calendar) + #expect(yearly.count == 6) + #expect(yearly[3].values[.codex] == 7) + #expect(yearly[4].values[.gemini] == 5) + #expect(yearly[5].values[.claude] == 40) +} + +@Test func dailyUsageDetailsAggregateProviderTotals() { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + let now = ISO8601DateFormatter().date(from: "2026-06-11T12:30:00Z")! + let records = [ + usage(.claude, "2026-06-11T01:00:00Z", 10, 2, 1), + usage(.codex, "2026-06-11T10:00:00Z", 20, 3, 0), + usage(.gemini, "2026-06-10T01:00:00Z", 5, 1, 0) + ] + let summary = UsageSummary.make(records: records, now: now, calendar: calendar) + let details = summary.dailyUsageDetails(calendar: calendar) + + #expect(details.count == 2) + #expect(details[0].totalTokens == 36) + #expect(details[0].inputTokens == 30) + #expect(details[0].outputTokens == 5) + #expect(details[0].cachedTokens == 1) + #expect(details[0].providerTotals[.claude] == 13) + #expect(details[0].providerTotals[.codex] == 23) + #expect(details[1].totalTokens == 6) + #expect(details[1].providerTotals[.gemini] == 6) +} + +@Test func scannerIndexesLogsThroughSQLiteCache() async throws { + let root = FileManager.default.temporaryDirectory + .appending(path: "TokenLensTests-\(UUID().uuidString)") + defer { try? FileManager.default.removeItem(at: root) } + + let sessions = root.appending(path: ".codex/sessions") + try FileManager.default.createDirectory(at: sessions, withIntermediateDirectories: true) + let file = sessions.appending(path: "rollout.jsonl") + let line = """ + {"timestamp":"2026-06-11T03:10:00.000Z","type":"event_msg","payload":{"info":{"last_token_usage":{"input_tokens":100,"output_tokens":30,"cached_input_tokens":20,"reasoning_output_tokens":4}}}} + """ + try line.write(to: file, atomically: true, encoding: .utf8) + + let scanner = TokenUsageScanner( + homeDirectory: root, + cacheURL: root.appending(path: "Library/Application Support/TokenLens/test-cache.sqlite") + ) + let now = ISO8601DateFormatter().date(from: "2026-06-11T12:30:00Z")! + + let first = await scanner.scan(now: now) + let second = await scanner.scan(now: now) + let appendedLine = """ + {"timestamp":"2026-06-11T03:12:00.000Z","type":"event_msg","payload":{"info":{"last_token_usage":{"input_tokens":200,"output_tokens":60,"cached_input_tokens":40,"reasoning_output_tokens":8}}}} + """ + let handle = try FileHandle(forWritingTo: file) + try handle.seekToEnd() + try handle.write(contentsOf: Data(("\n" + appendedLine).utf8)) + try handle.close() + let third = await scanner.scan(now: now) + + #expect(first.cards.totalTokens == 154) + #expect(second.cards.totalTokens == 154) + #expect(second.providerTotals[.codex]?.totalTokens == 154) + #expect(third.cards.totalTokens == 462) + #expect(third.providerTotals[.codex]?.totalTokens == 462) +} + +@Test func scannerIndexesHermesAndOpenCodeSQLiteSources() async throws { + let root = FileManager.default.temporaryDirectory + .appending(path: "TokenLensSQLiteSources-\(UUID().uuidString)") + defer { try? FileManager.default.removeItem(at: root) } + + let hermes = root.appending(path: ".hermes") + try FileManager.default.createDirectory(at: hermes, withIntermediateDirectories: true) + try makeHermesStateDB(at: hermes.appending(path: "state.db")) + + let opencode = root.appending(path: ".local/share/opencode") + try FileManager.default.createDirectory(at: opencode, withIntermediateDirectories: true) + try makeOpenCodeDB(at: opencode.appending(path: "opencode.db")) + + let scanner = TokenUsageScanner( + homeDirectory: root, + cacheURL: root.appending(path: "Library/Application Support/TokenLens/test-cache.sqlite") + ) + let now = Date(timeIntervalSince1970: 1_780_938_000) + let summary = await scanner.scan(now: now) + + #expect(summary.providerTotals[.hermes]?.totalTokens == 23) + #expect(summary.providerTotals[.opencode]?.totalTokens == 41) + #expect(summary.cards.totalTokens == 64) + #expect(summary.toolRows.map(\.provider).contains(.hermes)) + #expect(summary.toolRows.map(\.provider).contains(.opencode)) +} + +private func usage(_ provider: Provider, _ timestamp: String, _ input: Int, _ output: Int, _ cached: Int) -> UsageRecord { + UsageRecord( + provider: provider, + source: "\(provider.rawValue) source", + sourcePath: "/tmp/\(provider.rawValue).jsonl", + timestamp: ISO8601DateFormatter().date(from: timestamp)!, + sessionID: "", + model: provider.rawValue, + inputTokens: input, + outputTokens: output, + cachedTokens: cached, + reasoningTokens: 0, + toolTokens: 0 + ) +} + +private func makeHermesStateDB(at url: URL) throws { + let statements = [ + """ + CREATE TABLE sessions ( + id TEXT PRIMARY KEY, + source TEXT NOT NULL, + model TEXT, + started_at REAL NOT NULL, + input_tokens INTEGER DEFAULT 0, + output_tokens INTEGER DEFAULT 0, + cache_read_tokens INTEGER DEFAULT 0, + cache_write_tokens INTEGER DEFAULT 0, + reasoning_tokens INTEGER DEFAULT 0 + ) + """, + """ + INSERT INTO sessions ( + id, source, model, started_at, input_tokens, output_tokens, + cache_read_tokens, cache_write_tokens, reasoning_tokens + ) VALUES ('h1', 'cli', 'gpt-5.5', 1780937894.0, 10, 5, 3, 2, 3) + """ + ] + try writeSQLiteDatabase(at: url, statements: statements) +} + +private func makeOpenCodeDB(at url: URL) throws { + let statements = [ + """ + CREATE TABLE session ( + id TEXT PRIMARY KEY, + time_created INTEGER NOT NULL, + time_updated INTEGER NOT NULL, + model TEXT, + tokens_input INTEGER DEFAULT 0 NOT NULL, + tokens_output INTEGER DEFAULT 0 NOT NULL, + tokens_reasoning INTEGER DEFAULT 0 NOT NULL, + tokens_cache_read INTEGER DEFAULT 0 NOT NULL, + tokens_cache_write INTEGER DEFAULT 0 NOT NULL + ) + """, + """ + INSERT INTO session ( + id, time_created, time_updated, model, tokens_input, tokens_output, + tokens_reasoning, tokens_cache_read, tokens_cache_write + ) VALUES ( + 'o1', 1780937894000, 1780937895000, + '{"id":"MiniMax-M3","providerID":"minimax-cn"}', + 11, 7, 5, 13, 5 + ) + """ + ] + try writeSQLiteDatabase(at: url, statements: statements) +} + +private func writeSQLiteDatabase(at url: URL, statements: [String]) throws { + var database: OpaquePointer? + guard sqlite3_open_v2(url.path, &database, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nil) == SQLITE_OK else { + sqlite3_close(database) + struct OpenError: Error {} + throw OpenError() + } + defer { sqlite3_close(database) } + for statement in statements { + guard sqlite3_exec(database, statement, nil, nil, nil) == SQLITE_OK else { + struct ExecuteError: Error {} + throw ExecuteError() + } + } +} diff --git a/NativeTokenLens/Tools/GenerateAppIcon.swift b/NativeTokenLens/Tools/GenerateAppIcon.swift new file mode 100644 index 0000000..52b02b5 --- /dev/null +++ b/NativeTokenLens/Tools/GenerateAppIcon.swift @@ -0,0 +1,175 @@ +import AppKit +import Foundation + +struct IconRenderer { + let size: CGFloat + + func render(to url: URL) throws { + guard + let bitmap = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: Int(size), + pixelsHigh: Int(size), + bitsPerSample: 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .deviceRGB, + bytesPerRow: 0, + bitsPerPixel: 0 + ), + let graphics = NSGraphicsContext(bitmapImageRep: bitmap) + else { return } + + NSGraphicsContext.saveGraphicsState() + NSGraphicsContext.current = graphics + defer { NSGraphicsContext.restoreGraphicsState() } + + let context = graphics.cgContext + context.setShouldAntialias(true) + context.setAllowsAntialiasing(true) + + let rect = CGRect(x: 0, y: 0, width: size, height: size) + let scale = size / 1024 + let radius = 226 * scale + let base = CGPath( + roundedRect: rect.insetBy(dx: 44 * scale, dy: 44 * scale), + cornerWidth: radius, + cornerHeight: radius, + transform: nil + ) + + context.addPath(base) + context.clip() + + let background = CGGradient( + colorsSpace: CGColorSpaceCreateDeviceRGB(), + colors: [ + NSColor(calibratedRed: 0.04, green: 0.12, blue: 0.24, alpha: 1).cgColor, + NSColor(calibratedRed: 0.12, green: 0.20, blue: 0.42, alpha: 1).cgColor, + NSColor(calibratedRed: 0.58, green: 0.16, blue: 0.88, alpha: 1).cgColor + ] as CFArray, + locations: [0, 0.52, 1] + )! + context.drawLinearGradient( + background, + start: CGPoint(x: rect.minX, y: rect.maxY), + end: CGPoint(x: rect.maxX, y: rect.minY), + options: [] + ) + + drawGlow(context, color: NSColor.systemBlue, center: CGPoint(x: 260 * scale, y: 760 * scale), radius: 330 * scale) + drawGlow(context, color: NSColor.systemPurple, center: CGPoint(x: 785 * scale, y: 285 * scale), radius: 430 * scale) + drawGlassHighlight(context, rect: rect, scale: scale) + drawBars(context, scale: scale) + drawLens(context, scale: scale) + + context.resetClip() + context.addPath(base) + context.setStrokeColor(NSColor.white.withAlphaComponent(0.22).cgColor) + context.setLineWidth(12 * scale) + context.strokePath() + + guard let png = bitmap.representation(using: .png, properties: [:]) else { return } + try png.write(to: url) + } + + private func drawGlow(_ context: CGContext, color: NSColor, center: CGPoint, radius: CGFloat) { + let gradient = CGGradient( + colorsSpace: CGColorSpaceCreateDeviceRGB(), + colors: [ + color.withAlphaComponent(0.55).cgColor, + color.withAlphaComponent(0.0).cgColor + ] as CFArray, + locations: [0, 1] + )! + context.drawRadialGradient( + gradient, + startCenter: center, + startRadius: 0, + endCenter: center, + endRadius: radius, + options: [] + ) + } + + private func drawGlassHighlight(_ context: CGContext, rect: CGRect, scale: CGFloat) { + let highlight = CGGradient( + colorsSpace: CGColorSpaceCreateDeviceRGB(), + colors: [ + NSColor.white.withAlphaComponent(0.32).cgColor, + NSColor.white.withAlphaComponent(0.06).cgColor, + NSColor.white.withAlphaComponent(0.0).cgColor + ] as CFArray, + locations: [0, 0.36, 1] + )! + let path = CGPath( + roundedRect: CGRect(x: 120 * scale, y: 610 * scale, width: 790 * scale, height: 268 * scale), + cornerWidth: 130 * scale, + cornerHeight: 130 * scale, + transform: nil + ) + context.saveGState() + context.addPath(path) + context.clip() + context.drawLinearGradient( + highlight, + start: CGPoint(x: rect.minX, y: rect.maxY), + end: CGPoint(x: rect.maxX, y: rect.minY), + options: [] + ) + context.restoreGState() + } + + private func drawBars(_ context: CGContext, scale: CGFloat) { + let bars = [ + CGRect(x: 288, y: 286, width: 86, height: 300), + CGRect(x: 430, y: 212, width: 86, height: 448), + CGRect(x: 572, y: 330, width: 86, height: 252) + ].map { CGRect(x: $0.minX * scale, y: $0.minY * scale, width: $0.width * scale, height: $0.height * scale) } + + for (index, bar) in bars.enumerated() { + let color: NSColor = index == 1 ? .white : NSColor(calibratedWhite: 1, alpha: 0.88) + let path = CGPath( + roundedRect: bar, + cornerWidth: 44 * scale, + cornerHeight: 44 * scale, + transform: nil + ) + context.addPath(path) + context.setFillColor(color.cgColor) + context.fillPath() + } + } + + private func drawLens(_ context: CGContext, scale: CGFloat) { + let ring = CGRect(x: 682 * scale, y: 228 * scale, width: 170 * scale, height: 170 * scale) + context.setStrokeColor(NSColor.white.withAlphaComponent(0.92).cgColor) + context.setLineWidth(34 * scale) + context.strokeEllipse(in: ring) + context.setLineCap(.round) + context.move(to: CGPoint(x: 805 * scale, y: 275 * scale)) + context.addLine(to: CGPoint(x: 892 * scale, y: 188 * scale)) + context.strokePath() + } +} + +let outputRoot = URL(fileURLWithPath: CommandLine.arguments.dropFirst().first ?? "NativeTokenLens/Assets/AppIcon.iconset") +try FileManager.default.createDirectory(at: outputRoot, withIntermediateDirectories: true) + +let specs: [(String, CGFloat)] = [ + ("icon_16x16.png", 16), + ("icon_16x16@2x.png", 32), + ("icon_32x32.png", 32), + ("icon_32x32@2x.png", 64), + ("icon_128x128.png", 128), + ("icon_128x128@2x.png", 256), + ("icon_256x256.png", 256), + ("icon_256x256@2x.png", 512), + ("icon_512x512.png", 512), + ("icon_512x512@2x.png", 1024) +] + +for spec in specs { + try IconRenderer(size: spec.1).render(to: outputRoot.appendingPathComponent(spec.0)) +} diff --git a/design/tokenlens-dashboard.html b/design/tokenlens-dashboard.html new file mode 100644 index 0000000..8629bd0 --- /dev/null +++ b/design/tokenlens-dashboard.html @@ -0,0 +1,922 @@ + + + + + + TokenLens Dashboard Concept + + + +
+ + +
+
+
+
+ +
+ TokenLens +
+ +
+
+ + Last 30 Days + +
+
+ + Sync +
+ +
+
JD
+ +
+
+
+ +
+
+
+
+ +
+
+
今日 Tokens ⓘ
+
4.8M
+
↑ 12.8% vs. yesterday
+
+
+ +
+
+ +
+
+
本月 Tokens ⓘ
+
128.4M
+
↑ 9.7% vs. last month
+
+
+ +
+
+ +
+
+
历史总量 ⓘ
+
1.02B
+
3,842 sessions scanned
+
+
+ +
+
+ +
+
+
缓存命中 ⓘ
+
42%
+
↑ 8.6% saved context load
+
+
+
+ +
+
+
+
Token Usage Trend
+
Daily
+
+
+ + + + + + + 10M + 8M + 6M + 4M + 0 + May 12 + May 18 + May 24 + May 30 + Jun 06 + + + + + + + + + +
+
+ Claude + Codex + Gemini +
+
+ +
+
+
Usage Share
+
By Tokens
+
+ +
+
+ +
+
+
+
Provider Breakdown
+
All sources
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProviderSourceInputOutputCachedTotalTrend
Claude
projects/*.jsonl28.7M18.2M12.2M59.1M
Codex
sessions/*.jsonl18.9M13.4M3.6M35.9M
Gemini
tmp/chats/*.jsonl17.2M10.6M4.8M33.4M
Claude
HUD cache8.8M5.4M9.1M23.3M
+
+ Showing 4 local sources · content is never uploaded + Open raw report → +
+
+ +
+
+
Insights
+ View all +
+
+
+
!
+
1h
+
Claude usage exceeded daily average
+
Today is 80% above the 7-day baseline.
+
+
+
+
2h
+
Gemini source parsed cleanly
+
2 chat files scanned, no malformed rows.
+
+
+
+
3h
+
Codex reasoning tokens spiked today
+
Reasoning share is up 32% vs. yesterday.
+
+
+
+
+
+
+
+ + diff --git a/index.html b/index.html new file mode 100644 index 0000000..1c2c066 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + TokenLens + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..27d5c38 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5612 @@ +{ + "name": "tokenlens", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tokenlens", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-react": "^5.1.1", + "lucide-react": "^0.561.0", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "typescript": "^5.9.3", + "vite": "^7.2.7" + }, + "devDependencies": { + "electron": "^39.2.7", + "electron-builder": "^26.0.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/fuses/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", + "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz", + "integrity": "sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz", + "integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-lib": { + "version": "26.15.2", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.2.tgz", + "integrity": "sha512-3mYfKOjr/ZY7gFESOcq8kylBMgGPpmlQYnpBVit4p6zIg0t/8bkWBILdMMtnjFyN2jllyBf225T8dLlz3D6oBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.4", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@noble/hashes": "^2.2.0", + "@peculiar/webcrypto": "^1.7.1", + "@types/fs-extra": "9.0.13", + "ajv": "^8.18.0", + "asn1js": "^3.0.10", + "async-exit-hook": "^2.0.1", + "builder-util": "26.15.0", + "builder-util-runtime": "9.7.0", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.15.1", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.2.5", + "pkijs": "^3.4.0", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "unzipper": "^0.12.3", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.15.2", + "electron-builder-squirrel-windows": "26.15.2" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", + "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.15.0", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.0.tgz", + "integrity": "sha512-dUx+HxVbiNsNQ4mGe1PyoC/tBmsHwBNDLdBuqWCj+rhHFE9lHgrXiGYKAM1uNlznhAaUSyMlms84VeSSr3gOBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001797", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", + "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.15.2", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.2.tgz", + "integrity": "sha512-fMkjRqKyPtsz4Kzu/qGP0BGjqzMCIgp+/7kw/u6YH6lvn/8hvL3c0TXhoFayBoYdpPCnEinnCHztd4bW7/jetA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.2", + "builder-util": "26.15.0", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "39.8.10", + "resolved": "https://registry.npmjs.org/electron/-/electron-39.8.10.tgz", + "integrity": "sha512-zbYtGPYUI7PzqLAzkk21Rk6j67WN0hxn0Mq/njErZo1d0HSf33is4f8ICI5fMLy5vYe0JtCtM5sYunNOaochSQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.15.2", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.2.tgz", + "integrity": "sha512-veKM9+dCljaC5A74Pwc0ZWQ9arOHREXWh9hUIf8NGg49ch7x+IB4QhbMzIrV5ONZIXM2OEkaxW11cAPjPtoi4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.2", + "builder-util": "26.15.0", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.15.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.15.2", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.2.tgz", + "integrity": "sha512-PNl+SSRoma9mXhxycNGxutZPgvmK19v41mn8F9oecpAU2QNAldpB4HfMuA1LwFC2j8aRzzV5M9HKlKe6dfpvNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.15.2", + "builder-util": "26.15.0", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "26.15.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.1.tgz", + "integrity": "sha512-BMgMHOyexWn0UnOC+Afffw0DMrr0yfLp4U8YsLXwoJ3Da7LS7WUnz21teYZqO0gaApE1KgsjREWmbPqvF5JcPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "aws4": "^1.13.2", + "builder-util": "26.15.0", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.371", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", + "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", + "license": "ISC" + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "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" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.561.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.561.0.tgz", + "integrity": "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-abi": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.31.0.tgz", + "integrity": "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "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" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", + "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", + "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "^11.2.0", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/unzipper/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/unzipper/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..67ce51d --- /dev/null +++ b/package.json @@ -0,0 +1,60 @@ +{ + "name": "tokenlens", + "version": "0.1.0", + "description": "Local Claude, Codex, and Gemini token usage dashboard for macOS.", + "main": "src/main/main.cjs", + "type": "module", + "private": true, + "scripts": { + "test": "node --test tests/*.test.cjs tests/*.test.mjs", + "dev": "vite --host 127.0.0.1", + "electron:dev": "TOKENLENS_DEV_SERVER=http://127.0.0.1:5173 electron .", + "build": "vite build", + "dist": "npm run test && npm run build && electron-builder --mac dmg" + }, + "dependencies": { + "@vitejs/plugin-react": "^5.1.1", + "vite": "^7.2.7", + "typescript": "^5.9.3", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "lucide-react": "^0.561.0" + }, + "devDependencies": { + "electron": "^39.2.7", + "electron-builder": "^26.0.12" + }, + "build": { + "appId": "com.caoxiaozhu.tokenlens", + "productName": "TokenLens", + "directories": { + "output": "release" + }, + "files": [ + "dist/**", + "src/main/**", + "src/lib/**", + "package.json" + ], + "mac": { + "category": "public.app-category.developer-tools", + "target": [ + "dmg" + ] + }, + "dmg": { + "contents": [ + { + "x": 130, + "y": 220 + }, + { + "x": 410, + "y": 220, + "type": "link", + "path": "/Applications" + } + ] + } + } +} diff --git a/src/lib/tokenUsage.cjs b/src/lib/tokenUsage.cjs new file mode 100644 index 0000000..6336f6c --- /dev/null +++ b/src/lib/tokenUsage.cjs @@ -0,0 +1,383 @@ +const fs = require("node:fs/promises"); +const path = require("node:path"); +const os = require("node:os"); + +const PROVIDERS = ["Claude", "Codex", "Gemini"]; + +async function scanUsage(options = {}) { + const homeDir = options.homeDir || os.homedir(); + const timeZone = options.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Shanghai"; + const now = options.now || new Date(); + + const records = []; + const diagnostics = []; + + await collectProvider({ + root: path.join(homeDir, ".claude", "projects"), + matcher: (file) => file.endsWith(".jsonl"), + parseLine: parseClaudeLine, + records, + diagnostics + }); + + await collectProvider({ + root: path.join(homeDir, ".codex", "sessions"), + matcher: (file) => file.endsWith(".jsonl"), + parseLine: parseCodexLine, + records, + diagnostics + }); + + await collectProvider({ + root: path.join(homeDir, ".gemini"), + matcher: (file) => file.endsWith(".jsonl") && file.includes(`${path.sep}chats${path.sep}`), + parseLine: parseGeminiLine, + records, + diagnostics + }); + + const summary = summarizeRecords(records, { now, timeZone }); + return { + ...summary, + diagnostics + }; +} + +async function collectProvider({ root, matcher, parseLine, records, diagnostics }) { + const files = await listFiles(root).catch((error) => { + diagnostics.push({ + level: "warn", + source: root, + message: error.code === "ENOENT" ? "目录不存在" : error.message + }); + return []; + }); + + for (const file of files) { + if (!matcher(file)) continue; + const text = await fs.readFile(file, "utf8").catch((error) => { + diagnostics.push({ level: "warn", source: file, message: error.message }); + return ""; + }); + for (const line of text.split(/\r?\n/)) { + if (!line.trim()) continue; + const record = parseLine(line, file); + if (record) records.push(record); + } + } +} + +async function listFiles(root) { + const out = []; + async function walk(dir) { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + await walk(fullPath); + } else if (entry.isFile()) { + out.push(fullPath); + } + } + } + await walk(root); + return out; +} + +function parseClaudeLine(line, sourceFile) { + const obj = parseJson(line); + const usage = obj?.message?.usage || obj?.usage; + if (!usage) return null; + + const inputTokens = intValue(usage.input_tokens); + const outputTokens = intValue(usage.output_tokens); + const cachedTokens = + intValue(usage.cache_read_input_tokens) + + intValue(usage.cache_creation_input_tokens); + + return normalizeRecord({ + provider: "Claude", + source: sourceLabel(sourceFile, "Claude"), + sourceFile, + timestamp: obj.timestamp || obj.createdAt || obj.date, + sessionId: obj.sessionId, + cwd: obj.cwd, + model: obj.message?.model || obj.model || "Claude", + inputTokens, + outputTokens, + cachedTokens, + reasoningTokens: 0, + toolTokens: 0 + }); +} + +function parseCodexLine(line, sourceFile) { + const obj = parseJson(line); + const usage = obj?.payload?.info?.last_token_usage; + if (!usage) return null; + + return normalizeRecord({ + provider: "Codex", + source: sourceLabel(sourceFile, "Codex"), + sourceFile, + timestamp: obj.timestamp, + sessionId: obj.payload?.id || obj.payload?.thread_id, + cwd: obj.payload?.cwd, + model: obj.payload?.model || "Codex", + inputTokens: intValue(usage.input_tokens), + outputTokens: intValue(usage.output_tokens), + cachedTokens: intValue(usage.cached_input_tokens), + reasoningTokens: intValue(usage.reasoning_output_tokens), + toolTokens: 0 + }); +} + +function parseGeminiLine(line, sourceFile) { + const obj = parseJson(line); + const tokens = obj?.tokens; + if (!tokens) return null; + + return normalizeRecord({ + provider: "Gemini", + source: sourceLabel(sourceFile, "Gemini"), + sourceFile, + timestamp: obj.timestamp, + sessionId: obj.id, + cwd: obj.cwd, + model: obj.model || "Gemini", + inputTokens: intValue(tokens.input), + outputTokens: intValue(tokens.output), + cachedTokens: intValue(tokens.cached), + reasoningTokens: intValue(tokens.thoughts), + toolTokens: intValue(tokens.tool), + explicitTotal: intValue(tokens.total) + }); +} + +function normalizeRecord(input) { + const timestamp = input.timestamp ? new Date(input.timestamp) : null; + if (!timestamp || Number.isNaN(timestamp.getTime())) return null; + + const computedTotal = + input.inputTokens + + input.outputTokens + + input.cachedTokens + + input.reasoningTokens + + input.toolTokens; + + return { + provider: input.provider, + source: input.source, + sourceFile: input.sourceFile, + timestamp: timestamp.toISOString(), + sessionId: input.sessionId || "", + cwd: input.cwd || "", + model: input.model || input.provider, + inputTokens: input.inputTokens, + outputTokens: input.outputTokens, + cachedTokens: input.cachedTokens, + reasoningTokens: input.reasoningTokens, + toolTokens: input.toolTokens, + totalTokens: input.explicitTotal || computedTotal + }; +} + +function summarizeRecords(records, options = {}) { + const now = options.now || new Date(); + const timeZone = options.timeZone || "Asia/Shanghai"; + const todayKey = dateKey(now, timeZone); + const monthKey = todayKey.slice(0, 7); + + const providerTotals = Object.fromEntries(PROVIDERS.map((provider) => [provider, emptyBucket(provider)])); + const sourceBuckets = new Map(); + const dailyBuckets = new Map(); + let todayTokens = 0; + let monthTokens = 0; + let totalTokens = 0; + let cachedTokens = 0; + let inputTokens = 0; + let outputTokens = 0; + let reasoningTokens = 0; + + for (const record of records) { + const key = dateKey(new Date(record.timestamp), timeZone); + const recordMonth = key.slice(0, 7); + const total = record.totalTokens; + + totalTokens += total; + cachedTokens += record.cachedTokens; + inputTokens += record.inputTokens; + outputTokens += record.outputTokens; + reasoningTokens += record.reasoningTokens; + if (key === todayKey) todayTokens += total; + if (recordMonth === monthKey) monthTokens += total; + + addToBucket(providerTotals[record.provider] || (providerTotals[record.provider] = emptyBucket(record.provider)), record); + + const sourceKey = `${record.provider}|${record.source}`; + if (!sourceBuckets.has(sourceKey)) { + sourceBuckets.set(sourceKey, { + provider: record.provider, + source: record.source, + model: record.model, + sourceFile: record.sourceFile, + ...emptyTokenFields() + }); + } + addToBucket(sourceBuckets.get(sourceKey), record); + + if (!dailyBuckets.has(key)) { + dailyBuckets.set(key, Object.fromEntries(PROVIDERS.map((provider) => [provider, 0]))); + } + dailyBuckets.get(key)[record.provider] = (dailyBuckets.get(key)[record.provider] || 0) + total; + } + + const dailyTrend = Array.from(dailyBuckets.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .slice(-30) + .map(([date, values]) => ({ date, ...values })); + + const sourceRows = Array.from(sourceBuckets.values()) + .sort((a, b) => b.totalTokens - a.totalTokens) + .slice(0, 8); + + const cacheHitRate = totalTokens > 0 ? Math.round((cachedTokens / totalTokens) * 100) : 0; + + return { + generatedAt: now.toISOString(), + timeZone, + records, + cards: { + todayTokens, + monthTokens, + totalTokens, + cacheHitRate, + inputTokens, + outputTokens, + cachedTokens, + reasoningTokens, + sessionCount: new Set(records.map((record) => record.sessionId || record.sourceFile)).size + }, + providerTotals, + dailyTrend, + sourceRows, + insights: buildInsights({ todayTokens, monthTokens, totalTokens, cacheHitRate, providerTotals, records }) + }; +} + +function buildInsights({ todayTokens, cacheHitRate, providerTotals, records }) { + const topProvider = Object.values(providerTotals) + .filter((bucket) => bucket.totalTokens > 0) + .sort((a, b) => b.totalTokens - a.totalTokens)[0]; + + const insights = []; + if (topProvider) { + insights.push({ + tone: "warn", + title: `${topProvider.provider} 是当前主要用量来源`, + copy: `已从本地日志统计 ${formatTokens(topProvider.totalTokens)} Tokens。` + }); + } + if (cacheHitRate > 0) { + insights.push({ + tone: "good", + title: "检测到缓存 Tokens", + copy: `当前 ${cacheHitRate}% 的统计用量来自缓存上下文。` + }); + } + insights.push({ + tone: "spike", + title: "今日用量快照已更新", + copy: `当前本地日期已统计 ${formatTokens(todayTokens)} Tokens。` + }); + insights.push({ + tone: "info", + title: "仅执行本地扫描", + copy: `已解析 ${records.length} 条用量记录,未上传对话内容。` + }); + return insights.slice(0, 4); +} + +function addToBucket(bucket, record) { + bucket.inputTokens += record.inputTokens; + bucket.outputTokens += record.outputTokens; + bucket.cachedTokens += record.cachedTokens; + bucket.reasoningTokens += record.reasoningTokens; + bucket.toolTokens += record.toolTokens; + bucket.totalTokens += record.totalTokens; + bucket.count += 1; +} + +function emptyBucket(provider) { + return { + provider, + ...emptyTokenFields() + }; +} + +function emptyTokenFields() { + return { + inputTokens: 0, + outputTokens: 0, + cachedTokens: 0, + reasoningTokens: 0, + toolTokens: 0, + totalTokens: 0, + count: 0 + }; +} + +function dateKey(date, timeZone) { + const parts = new Intl.DateTimeFormat("en-CA", { + timeZone, + year: "numeric", + month: "2-digit", + day: "2-digit" + }).formatToParts(date); + const values = Object.fromEntries(parts.map((part) => [part.type, part.value])); + return `${values.year}-${values.month}-${values.day}`; +} + +function sourceLabel(sourceFile, provider) { + if (provider === "Claude" && sourceFile.includes(`${path.sep}.claude${path.sep}projects${path.sep}`)) { + return "projects/*.jsonl"; + } + if (provider === "Codex" && sourceFile.includes(`${path.sep}.codex${path.sep}sessions${path.sep}`)) { + return "sessions/*.jsonl"; + } + if (provider === "Gemini" && sourceFile.includes(`${path.sep}chats${path.sep}`)) { + return "tmp/chats/*.jsonl"; + } + return path.basename(sourceFile); +} + +function parseJson(line) { + try { + return JSON.parse(line); + } catch { + return null; + } +} + +function intValue(value) { + return Number.isFinite(Number(value)) ? Number(value) : 0; +} + +function formatTokens(value) { + if (value >= 100_000_000) return trimNumber(value / 100_000_000, 2) + "亿"; + if (value >= 10_000) return trimNumber(value / 10_000, 1) + "万"; + return String(value); +} + +function trimNumber(value, digits) { + return value.toFixed(digits).replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1"); +} + +module.exports = { + parseClaudeLine, + parseCodexLine, + parseGeminiLine, + summarizeRecords, + scanUsage, + formatTokens +}; diff --git a/src/main/main.cjs b/src/main/main.cjs new file mode 100644 index 0000000..9b5fce9 --- /dev/null +++ b/src/main/main.cjs @@ -0,0 +1,44 @@ +const { app, BrowserWindow, ipcMain } = require("electron"); +const path = require("node:path"); +const { scanUsage } = require("../lib/tokenUsage.cjs"); + +let mainWindow; + +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1440, + height: 980, + minWidth: 1180, + minHeight: 820, + title: "TokenLens", + backgroundColor: "#08111d", + titleBarStyle: "hiddenInset", + trafficLightPosition: { x: 20, y: 20 }, + webPreferences: { + preload: path.join(__dirname, "preload.cjs"), + contextIsolation: true, + nodeIntegration: false + } + }); + + const devServer = process.env.TOKENLENS_DEV_SERVER; + if (devServer) { + mainWindow.loadURL(devServer); + } else { + mainWindow.loadFile(path.join(__dirname, "../../dist/index.html")); + } +} + +ipcMain.handle("usage:scan", async () => { + return scanUsage(); +}); + +app.whenReady().then(createWindow); + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") app.quit(); +}); + +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); +}); diff --git a/src/main/preload.cjs b/src/main/preload.cjs new file mode 100644 index 0000000..71f9c07 --- /dev/null +++ b/src/main/preload.cjs @@ -0,0 +1,5 @@ +const { contextBridge, ipcRenderer } = require("electron"); + +contextBridge.exposeInMainWorld("tokenLens", { + scanUsage: () => ipcRenderer.invoke("usage:scan") +}); diff --git a/src/renderer/App.jsx b/src/renderer/App.jsx new file mode 100644 index 0000000..fa831c2 --- /dev/null +++ b/src/renderer/App.jsx @@ -0,0 +1,460 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { createRoot } from "react-dom/client"; +import { + BarChart3, + CalendarDays, + ChevronDown, + Check, + Info, + Layers3, + RefreshCcw, + Sparkles, + TrendingUp +} from "lucide-react"; +import { sampleSummary } from "./sampleData"; +import { formatTokensZh } from "./displayFormat"; +import "./styles.css"; + +const providers = ["Claude", "Codex", "Gemini"]; +const providerColors = { + Claude: "#2e83ff", + Codex: "#9b5cf6", + Gemini: "#2dd4cf" +}; + +function App() { + const [summary, setSummary] = useState(sampleSummary); + const [status, setStatus] = useState("loading"); + const [lastUpdated, setLastUpdated] = useState(""); + const [refreshCount, setRefreshCount] = useState(0); + const [range, setRange] = useState("近 30 天"); + const [trendMode, setTrendMode] = useState("每日"); + const [shareMode, setShareMode] = useState("按 Tokens"); + const [toolFilter, setToolFilter] = useState("全部工具"); + const scanningRef = useRef(false); + + const refresh = useCallback(async ({ silent = false } = {}) => { + if (scanningRef.current) return; + scanningRef.current = true; + if (!silent) setStatus("loading"); + try { + if (window.tokenLens?.scanUsage) { + const data = await window.tokenLens.scanUsage(); + setSummary(data); + setStatus("ready"); + setLastUpdated(new Date(data.generatedAt).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })); + setRefreshCount((count) => count + 1); + } else { + setSummary(sampleSummary); + setStatus("preview"); + setLastUpdated("预览数据"); + } + } catch (error) { + setStatus("error"); + setLastUpdated("扫描失败"); + console.error(error); + } finally { + scanningRef.current = false; + } + }, []); + + useEffect(() => { + refresh(); + const timer = window.setInterval(() => { + refresh({ silent: true }); + }, 15_000); + return () => window.clearInterval(timer); + }, [refresh]); + + const totalByProvider = useMemo(() => providers.reduce((sum, provider) => { + return sum + (summary.providerTotals?.[provider]?.totalTokens || 0); + }, 0), [summary]); + + const sourceRows = summary.sourceRows?.length ? summary.sourceRows : sampleSummary.sourceRows; + const filteredRows = toolFilter === "全部工具" ? sourceRows : sourceRows.filter((row) => row.provider === toolFilter); + const insights = summary.insights?.length ? summary.insights.slice(0, 3) : sampleSummary.insights; + + return ( +
+
+ refresh()} + status={status} + lastUpdated={lastUpdated} + refreshCount={refreshCount} + range={range} + setRange={setRange} + /> +
+ + +
+ } + > + + + + } + > + + +
+ +
+ + +
+
+
+
+ ); +} + +function TopBar({ onRefresh, status, lastUpdated, refreshCount, range, setRange }) { + return ( +
+
+ + TokenLens 用量统计 + + + {status === "error" ? "扫描异常" : "每 15 秒自动刷新"} + +
+
+ } + className="toolbar-dropdown" + /> + +
+ {lastUpdated ? `上次更新 ${lastUpdated}` : "准备扫描"} + 已刷新 {refreshCount} 次 +
+
+
+ ); +} + +function MetricCards({ summary }) { + const cards = [ + { + label: "今日 Tokens", + value: formatTokensZh(summary.cards?.todayTokens || 0), + detail: "本地今日已统计", + trend: "实时", + tone: "blue", + icon: Layers3 + }, + { + label: "本月 Tokens", + value: formatTokensZh(summary.cards?.monthTokens || 0), + detail: "按本地时区汇总", + trend: "月度", + tone: "green", + icon: Sparkles + }, + { + label: "历史总量", + value: formatTokensZh(summary.cards?.totalTokens || 0), + detail: `已扫描 ${summary.cards?.sessionCount || 0} 个会话`, + trend: "全部", + tone: "violet", + icon: Check + }, + { + label: "缓存命中", + value: `${summary.cards?.cacheHitRate || 0}%`, + detail: "来自缓存上下文", + trend: "缓存", + tone: "orange", + icon: TrendingUp + } + ]; + + return ( +
+ {cards.map((card, index) => { + const Icon = card.icon; + return ( +
+
+ +
+
+
{card.label}
+
{card.value}
+
+ {card.trend} + {card.detail} +
+
+
+ ); + })} +
+ ); +} + +function Panel({ title, control, children }) { + return ( +
+
+

{title}

+ {control} +
+ {children} +
+ ); +} + +function Dropdown({ value, options, onChange, icon, className = "" }) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + function close(event) { + if (!ref.current?.contains(event.target)) setOpen(false); + } + document.addEventListener("pointerdown", close); + return () => document.removeEventListener("pointerdown", close); + }, []); + + return ( +
+ + {open ? ( +
+ {options.map((option) => ( + + ))} +
+ ) : null} +
+ ); +} + +function TrendChart({ data }) { + const chartData = data.length ? data.slice(-24) : sampleSummary.dailyTrend; + const width = 640; + const height = 235; + const left = 58; + const right = 18; + const top = 18; + const bottom = 34; + const maxValue = Math.max(1, ...chartData.flatMap((item) => providers.map((provider) => item[provider] || 0))); + const plotWidth = width - left - right; + const plotHeight = height - top - bottom; + + const x = (index) => left + (index / Math.max(chartData.length - 1, 1)) * plotWidth; + const y = (value) => top + plotHeight - (value / maxValue) * plotHeight; + const lines = providers.map((provider) => ({ + provider, + points: chartData.map((item, index) => `${x(index)},${y(item[provider] || 0)}`).join(" ") + })); + + const labelIndexes = [0, 6, 12, 18, chartData.length - 1].filter((index, pos, arr) => index >= 0 && arr.indexOf(index) === pos); + + return ( + <> + + {[0, 0.25, 0.5, 0.75, 1].map((step) => { + const lineY = top + plotHeight * step; + const value = maxValue * (1 - step); + return ( + + + {formatTokensZh(value)} + + ); + })} + {labelIndexes.map((index) => ( + + {shortDate(chartData[index]?.date)} + + ))} + {lines.map((line) => ( + + ))} + {lines.map((line) => chartData.filter((_, index) => index % 5 === 0 || index === chartData.length - 1).map((item, dotIndex) => ( + + )))} + + + + ); +} + +function UsageShare({ providerTotals, total }) { + const safeTotal = total || 1; + let cursor = 0; + const gradient = providers.map((provider) => { + const share = ((providerTotals[provider]?.totalTokens || 0) / safeTotal) * 360; + const start = cursor; + cursor += share; + return `${providerColors[provider]} ${start}deg ${cursor}deg`; + }).join(", "); + + return ( +
+
+
+ {formatTokensZh(total)} + 本月 +
+
+
+ {providers.map((provider) => { + const providerTotal = providerTotals[provider]?.totalTokens || 0; + const percent = total ? Math.round((providerTotal / total) * 100) : 0; + return ( +
+
+ {provider} + {formatTokensZh(providerTotal)} Tokens +
+ {percent}% +
+ ); + })} +
+
+ ); +} + +function ProviderBreakdown({ rows, toolFilter, setToolFilter }) { + return ( +
+
+

工具明细

+ +
+ + + + + + + + + + + + + {rows.map((row, index) => ( + + + + + + + + + + ))} + +
工具输入输出缓存总量趋势 +
{formatTokensZh(row.inputTokens)}{formatTokensZh(row.outputTokens)}{formatTokensZh(row.cachedTokens)}{formatTokensZh(row.totalTokens)}
+
+ 显示 {rows.length} 个本地工具 · 不上传对话内容 + +
+
+ ); +} + +function ProviderName({ provider }) { + return ( +
+ {provider === "Gemini" ? "" : provider[0]} + {provider} +
+ ); +} + +function Sparkline({ provider, seed }) { + const base = [ + [17, 12, 16, 8, 12, 6, 10, 4, 8, 5, 9], + [18, 11, 15, 17, 10, 13, 7, 12, 6, 8, 4], + [16, 16, 10, 18, 14, 9, 12, 6, 5, 8, 8] + ][seed % 3]; + const points = base.map((value, index) => `${index * 7},${value}`).join(" "); + return ( + + + + ); +} + +function Insights({ insights }) { + return ( +
+
+

洞察提醒

+ +
+
+ {insights.map((insight, index) => ( +
+
{insight.tone === "good" ? "✓" : insight.tone === "warn" ? "!" : "↗"}
+
{index + 1} 小时
+ {insight.title} + {insight.copy} +
+ ))} +
+
+ ); +} + +function Legend() { + return ( +
+ {providers.map((provider) => ( + {provider} + ))} +
+ ); +} + +function shortDate(value) { + if (!value) return ""; + const [, month, day] = value.split("-"); + return `${month}/${day}`; +} + +createRoot(document.getElementById("root")).render(); diff --git a/src/renderer/displayFormat.js b/src/renderer/displayFormat.js new file mode 100644 index 0000000..5f6099f --- /dev/null +++ b/src/renderer/displayFormat.js @@ -0,0 +1,10 @@ +export function formatTokensZh(value) { + const number = Number(value) || 0; + if (number >= 100_000_000) return trimNumber(number / 100_000_000, 2) + "亿"; + if (number >= 10_000) return trimNumber(number / 10_000, 1) + "万"; + return String(Math.round(number)); +} + +function trimNumber(value, digits) { + return value.toFixed(digits).replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1"); +} diff --git a/src/renderer/sampleData.js b/src/renderer/sampleData.js new file mode 100644 index 0000000..f20b830 --- /dev/null +++ b/src/renderer/sampleData.js @@ -0,0 +1,113 @@ +export const sampleSummary = { + generatedAt: new Date().toISOString(), + cards: { + todayTokens: 4_800_000, + monthTokens: 128_400_000, + totalTokens: 1_020_000_000, + cacheHitRate: 42, + sessionCount: 3842 + }, + providerTotals: { + Claude: { + provider: "Claude", + inputTokens: 28_700_000, + outputTokens: 18_200_000, + cachedTokens: 12_200_000, + reasoningTokens: 0, + totalTokens: 59_100_000, + count: 420 + }, + Codex: { + provider: "Codex", + inputTokens: 18_900_000, + outputTokens: 13_400_000, + cachedTokens: 3_600_000, + reasoningTokens: 1_200_000, + totalTokens: 35_900_000, + count: 260 + }, + Gemini: { + provider: "Gemini", + inputTokens: 17_200_000, + outputTokens: 10_600_000, + cachedTokens: 4_800_000, + reasoningTokens: 800_000, + totalTokens: 33_400_000, + count: 180 + } + }, + dailyTrend: buildTrend(), + sourceRows: [ + row("Claude", "projects/*.jsonl", 28_700_000, 18_200_000, 12_200_000, 59_100_000), + row("Codex", "sessions/*.jsonl", 18_900_000, 13_400_000, 3_600_000, 35_900_000), + row("Gemini", "tmp/chats/*.jsonl", 17_200_000, 10_600_000, 4_800_000, 33_400_000) + ], + insights: [ + { + tone: "warn", + title: "Claude Tokens 高于日均水平", + copy: "今日用量比 7 日基线高 80%。" + }, + { + tone: "good", + title: "Gemini 日志解析正常", + copy: "已扫描 2 个聊天文件,未发现异常行。" + }, + { + tone: "spike", + title: "Codex 推理 Tokens 今日上升", + copy: "推理占比较昨日提高 32%。" + } + ], + diagnostics: [] +}; + +function row(provider, source, inputTokens, outputTokens, cachedTokens, totalTokens) { + return { + provider, + source, + inputTokens, + outputTokens, + cachedTokens, + totalTokens, + reasoningTokens: 0, + toolTokens: 0, + count: 1 + }; +} + +function buildTrend() { + const values = [ + [6.7, 4.1, 3.1], + [7.4, 4.7, 3.4], + [7.0, 4.2, 2.9], + [8.0, 5.0, 3.7], + [6.7, 4.4, 3.1], + [7.9, 5.1, 3.4], + [7.5, 4.5, 2.9], + [6.7, 3.8, 2.7], + [7.8, 5.4, 3.1], + [8.8, 4.7, 4.0], + [7.4, 4.6, 3.1], + [6.5, 5.1, 3.4], + [8.7, 4.9, 4.2], + [7.5, 5.6, 3.5], + [7.1, 5.0, 2.9], + [8.5, 4.8, 3.5], + [9.1, 5.4, 4.0], + [8.7, 6.1, 3.4], + [8.9, 5.5, 3.2], + [9.3, 5.1, 3.5], + [7.5, 5.6, 3.8], + [8.2, 5.0, 3.4], + [7.9, 5.5, 3.6], + [8.6, 4.9, 3.0] + ]; + + return values.map(([Claude, Codex, Gemini], index) => ({ + date: `2026-06-${String(index + 1).padStart(2, "0")}`, + Claude: Claude * 1_000_000, + Codex: Codex * 1_000_000, + Gemini: Gemini * 1_000_000 + })); +} diff --git a/src/renderer/styles.css b/src/renderer/styles.css new file mode 100644 index 0000000..b0a4c9b --- /dev/null +++ b/src/renderer/styles.css @@ -0,0 +1,918 @@ +:root { + color-scheme: dark; + --bg: #08111d; + --panel: rgba(19, 31, 47, 0.9); + --panel-2: rgba(16, 26, 40, 0.96); + --line: rgba(151, 174, 205, 0.18); + --line-strong: rgba(163, 188, 224, 0.29); + --text: #f2f6ff; + --muted: #96a4b8; + --blue: #2e83ff; + --cyan: #2dd4cf; + --violet: #9b5cf6; + --green: #2ed06e; + --orange: #ff9a31; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-synthesis: none; + text-rendering: geometricPrecision; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 1180px; + min-height: 760px; + overflow: hidden; + color: var(--text); + background: + linear-gradient(120deg, rgba(255, 255, 255, 0.055), transparent 18%, rgba(255, 255, 255, 0.035) 31%, transparent 48%), + radial-gradient(ellipse at 28% 14%, rgba(46, 131, 255, 0.16), transparent 28%), + radial-gradient(ellipse at 72% 22%, rgba(45, 212, 207, 0.1), transparent 26%), + linear-gradient(135deg, #070d17 0%, #0a1422 48%, #07111d 100%); +} + +button { + border: 0; + color: inherit; + font: inherit; +} + +.app-shell { + width: 100vw; + height: 100vh; + display: grid; + grid-template-columns: minmax(0, 1fr); + overflow: hidden; + background: rgba(8, 17, 29, 0.9); +} + +.sidebar { + position: relative; + padding: 22px 0; + border-right: 1px solid var(--line); + background: + linear-gradient(180deg, rgba(14, 28, 48, 0.98), rgba(8, 17, 29, 0.98)), + radial-gradient(circle at 0 0, rgba(46, 131, 255, 0.18), transparent 38%); +} + +.traffic-lights { + display: flex; + gap: 12px; + padding: 12px 24px 30px; +} + +.traffic { + width: 16px; + height: 16px; + border-radius: 50%; +} + +.traffic.red { background: #ff5f57; } +.traffic.yellow { background: #ffbd2e; } +.traffic.green { background: #28c840; } + +.nav-list { + display: grid; + gap: 8px; + padding-left: 6px; +} + +.nav-item { + height: 54px; + display: flex; + align-items: center; + gap: 14px; + padding: 0 22px; + color: #c7d1df; + font-size: 16px; + font-weight: 650; + background: transparent; + border-left: 3px solid transparent; + cursor: default; +} + +.nav-item.active { + color: #fff; + border-left-color: var(--blue); + background: linear-gradient(90deg, rgba(46, 131, 255, 0.56), rgba(46, 131, 255, 0.2)); + border-radius: 8px 8px 8px 0; + box-shadow: inset 0 0 0 1px rgba(81, 143, 255, 0.2); +} + +.side-status { + position: absolute; + left: 24px; + right: 22px; + bottom: 34px; + display: grid; + gap: 14px; + color: var(--muted); + font-size: 13px; +} + +.status-line { + display: flex; + align-items: center; + gap: 10px; + white-space: nowrap; +} + +.status-line.muted { + color: #8593a7; +} + +.status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--green); + box-shadow: 0 0 0 5px rgba(46, 208, 110, 0.1); +} + +.status-dot.loading { + background: var(--orange); + box-shadow: 0 0 0 5px rgba(255, 154, 49, 0.12); +} + +.status-dot.error { + background: #ff6363; + box-shadow: 0 0 0 5px rgba(255, 99, 99, 0.12); +} + +.main-pane { + min-width: 0; + display: grid; + grid-template-rows: 74px minmax(0, 1fr); + background: linear-gradient(180deg, rgba(9, 18, 31, 0.62), rgba(8, 17, 29, 0.84)); +} + +.topbar { + position: relative; + z-index: 80; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 28px 0 92px; + border-bottom: 1px solid var(--line); + background: rgba(9, 17, 29, 0.52); + backdrop-filter: blur(26px) saturate(1.35); + box-shadow: inset 0 1px 0 rgba(255,255,255,0.08); + -webkit-app-region: drag; +} + +.brand, +.toolbar, +.toolbar-button, +.search-box, +.profile { + display: flex; + align-items: center; +} + +.brand { + gap: 13px; + font-size: 20px; + font-weight: 800; +} + +.live-pill { + height: 28px; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0 10px; + border: 1px solid rgba(46, 208, 110, 0.22); + border-radius: 999px; + color: #bcebd0; + background: rgba(46, 208, 110, 0.09); + font-size: 12px; + font-weight: 700; +} + +.live-pill i { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--green); + box-shadow: 0 0 0 5px rgba(46, 208, 110, 0.1); +} + +.live-pill.loading { + color: #ffd7a8; + border-color: rgba(255, 154, 49, 0.25); + background: rgba(255, 154, 49, 0.09); +} + +.live-pill.loading i { + background: var(--orange); + box-shadow: 0 0 0 5px rgba(255, 154, 49, 0.1); +} + +.live-pill.error { + color: #ffc3c3; + border-color: rgba(255, 99, 99, 0.25); + background: rgba(255, 99, 99, 0.1); +} + +.live-pill.error i { + background: #ff6363; + box-shadow: 0 0 0 5px rgba(255, 99, 99, 0.1); +} + +.toolbar { + gap: 14px; + -webkit-app-region: no-drag; +} + +.refresh-meta { + display: grid; + gap: 2px; + min-width: 112px; + color: #b9c5d6; + font-size: 12px; + line-height: 1.2; +} + +.refresh-meta small { + color: #7f8da3; + font-size: 11px; +} + +.toolbar-button, +.dropdown-trigger { + height: 42px; + gap: 10px; + padding: 0 14px; + border: 1px solid var(--line-strong); + border-radius: 8px; + color: #e9eef8; + background: linear-gradient(145deg, rgba(255,255,255,0.1), rgba(13,24,38,0.68)); + backdrop-filter: blur(20px) saturate(1.28); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.12), + 0 14px 30px rgba(0, 0, 0, 0.16); + transition: transform 180ms ease, border-color 180ms ease, background 180ms ease; +} + +.toolbar-button.primary { + background: linear-gradient(145deg, rgba(72, 93, 124, 0.68), rgba(28, 43, 63, 0.78)); +} + +.toolbar-button:hover, +.dropdown-trigger:hover { + transform: translateY(-1px); + border-color: rgba(179, 205, 239, 0.45); +} + +.dropdown { + position: relative; +} + +.dropdown-trigger { + display: inline-flex; + align-items: center; + min-width: 104px; + justify-content: center; +} + +.toolbar-dropdown .dropdown-trigger { + min-width: 138px; +} + +.chevron { + transition: transform 160ms ease; +} + +.chevron.open { + transform: rotate(180deg); +} + +.dropdown-menu { + position: absolute; + right: 0; + top: calc(100% + 8px); + z-index: 120; + min-width: 132px; + display: grid; + gap: 4px; + padding: 7px; + border: 1px solid rgba(190, 215, 248, 0.24); + border-radius: 10px; + background: linear-gradient(145deg, rgba(30, 45, 66, 0.82), rgba(10, 20, 34, 0.78)); + backdrop-filter: blur(24px) saturate(1.45); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.16), + 0 22px 60px rgba(0, 0, 0, 0.34); + animation: menuIn 140ms ease-out both; +} + +.dropdown-menu button { + height: 34px; + padding: 0 10px; + border-radius: 7px; + color: #d8e1ef; + background: transparent; + text-align: left; +} + +.dropdown-menu button:hover, +.dropdown-menu button.selected { + color: #fff; + background: rgba(46, 131, 255, 0.2); +} + +.profile { + gap: 10px; + padding: 0 4px; + background: transparent; +} + +.profile span { + width: 40px; + height: 40px; + border-radius: 50%; + display: grid; + place-items: center; + font-weight: 800; + background: linear-gradient(135deg, #7c4dff, #bc75ff); + box-shadow: 0 8px 24px rgba(124, 77, 255, 0.32); +} + +.dashboard { + position: relative; + z-index: 1; + min-height: 0; + overflow-y: auto; + padding: 18px 20px 14px 18px; + display: grid; + grid-template-rows: 125px 295px minmax(0, 1fr); + gap: 12px; +} + +.dashboard::-webkit-scrollbar { + width: 8px; +} + +.dashboard::-webkit-scrollbar-thumb { + background: rgba(151, 174, 205, 0.22); + border-radius: 999px; +} + +.metric-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 14px; +} + +.metric-card, +.panel { + position: relative; + border: 1px solid rgba(180, 205, 240, 0.3); + border-radius: 14px; + background: + linear-gradient(145deg, rgba(255,255,255,0.105), rgba(255,255,255,0.025) 38%, rgba(9,18,31,0.58)), + linear-gradient(180deg, rgba(24, 38, 57, 0.76), rgba(13, 24, 38, 0.72)); + backdrop-filter: blur(28px) saturate(1.38); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.16), + inset 0 -1px 0 rgba(255, 255, 255, 0.05), + 0 22px 60px rgba(0, 0, 0, 0.22); + overflow: hidden; + animation: cardIn 460ms ease-out both; +} + +.metric-card::before, +.panel::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background: + linear-gradient(115deg, rgba(255,255,255,0.16), transparent 18%, transparent 74%, rgba(255,255,255,0.06)), + linear-gradient(270deg, transparent, rgba(255,255,255,0.045), transparent); + opacity: 0.78; +} + +.metric-card:hover, +.panel:hover { + border-color: rgba(210, 230, 255, 0.42); + transform: translateY(-1px); + transition: transform 180ms ease, border-color 180ms ease; +} + +.metric-card { + display: grid; + grid-template-columns: 66px 1fr; + align-items: center; + padding: 0 20px; + min-width: 0; + animation-delay: calc(var(--i, 0) * 55ms); +} + +.metric-icon { + width: 52px; + height: 52px; + border-radius: 50%; + display: grid; + place-items: center; + color: white; + box-shadow: inset 0 0 28px rgba(255, 255, 255, 0.08); + animation: floatGlow 3.8s ease-in-out infinite; +} + +.metric-icon.blue { background: radial-gradient(circle at 35% 24%, #408dff, #123c88); } +.metric-icon.green { background: radial-gradient(circle at 35% 24%, #37d48b, #126545); } +.metric-icon.violet { background: radial-gradient(circle at 35% 24%, #a970ff, #42217b); } +.metric-icon.orange { background: radial-gradient(circle at 35% 24%, #e28b35, #6e3a0d); } + +.metric-copy { + min-width: 0; +} + +.metric-label { + display: flex; + gap: 6px; + align-items: center; + color: #c3ccda; + font-size: 14px; + margin-bottom: 6px; +} + +.metric-value { + font-size: clamp(27px, 2.5vw, 33px); + line-height: 1; + font-weight: 820; + margin-bottom: 10px; + white-space: nowrap; +} + +.metric-detail { + display: flex; + gap: 7px; + color: #aab4c5; + font-size: 14px; +} + +.blue { color: var(--blue); } +.green { color: var(--green); } +.violet { color: var(--violet); } +.orange { color: var(--orange); } + +.middle-grid { + min-height: 0; + display: grid; + grid-template-columns: 1.35fr 1fr; + gap: 12px; +} + +.bottom-grid { + min-height: 0; + display: grid; + grid-template-columns: 1.55fr 0.72fr; + gap: 12px; +} + +.panel { + min-width: 0; + min-height: 0; + padding: 16px 22px; + overflow: visible; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + position: relative; + z-index: 25; +} + +.panel-header h2 { + margin: 0; + font-size: 17px; + font-weight: 800; +} + +.mini-select, +.table-title button, +.table-footer button { + height: 32px; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0 11px; + border: 1px solid var(--line); + border-radius: 7px; + color: #dce4ef; + font-size: 13px; + background: rgba(8, 17, 29, 0.35); +} + +.trend-chart { + width: 100%; + height: calc(100% - 28px); + min-height: 178px; +} + +.grid-line { + stroke: rgba(151, 174, 205, 0.14); + stroke-width: 1; +} + +.axis-label { + fill: #93a0b3; + font-size: 13px; +} + +.trend-line { + fill: none; + stroke-width: 3; + stroke-linejoin: round; + stroke-linecap: round; + stroke-dasharray: 900; + stroke-dashoffset: 900; + animation: drawLine 900ms ease-out forwards; +} + +.legend { + display: flex; + align-items: center; + justify-content: center; + gap: 38px; + color: #cdd6e4; + font-size: 14px; +} + +.legend span, +.legend-name { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.legend i, +.legend-name i { + width: 11px; + height: 11px; + border-radius: 50%; + box-shadow: 0 0 10px color-mix(in srgb, currentColor 60%, transparent); +} + +.share-body { + height: calc(100% - 38px); + display: grid; + grid-template-columns: minmax(176px, 0.8fr) 1fr; + align-items: center; + gap: 18px; +} + +.donut { + width: min(185px, 100%); + aspect-ratio: 1; + border-radius: 50%; + display: grid; + place-items: center; + justify-self: center; + box-shadow: 0 0 34px rgba(46, 131, 255, 0.18); + position: relative; + animation: glassPulse 4.2s ease-in-out infinite; +} + +.donut::before { + content: ""; + position: absolute; + width: 53%; + height: 53%; + border-radius: 50%; + background: linear-gradient(145deg, #101f31, #0b1726); + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.12); +} + +.donut-center { + position: relative; + z-index: 1; + display: grid; + gap: 4px; + text-align: center; +} + +.donut-center strong { + font-size: 25px; +} + +.donut-center span { + color: #a8b4c5; + font-size: 14px; +} + +.share-list { + display: grid; + gap: 17px; +} + +.share-row { + display: grid; + grid-template-columns: 1fr 58px; + align-items: center; + gap: 10px; +} + +.share-row strong { + font-size: 20px; + text-align: right; +} + +.share-row small { + display: block; + margin-left: 19px; + margin-top: 2px; + color: #9ca8ba; +} + +.table-panel, +.insight-panel { + padding: 0; +} + +.table-title { + height: 48px; + margin: 0; + padding: 0 22px; + border-bottom: 1px solid var(--line); + position: relative; + z-index: 2; +} + +table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; + font-size: 14px; +} + +th, +td { + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +th { + height: 40px; + color: #9aa7b9; + font-weight: 650; + border-bottom: 1px solid var(--line); +} + +td { + height: 58px; + color: #d4dce8; + border-bottom: 1px solid rgba(151, 174, 205, 0.1); +} + +th:first-child, +td:first-child { + padding-left: 22px; +} + +th:not(:first-child), +td:not(:first-child) { + padding-left: 10px; +} + +th:nth-child(1) { width: 150px; } +th:nth-child(6) { width: 92px; } +th:nth-child(7) { width: 38px; } + +.provider-name { + display: flex; + align-items: center; + gap: 9px; + min-width: 0; + color: #eef4ff; + font-weight: 720; +} + +.provider-logo { + width: 28px; + height: 28px; + border-radius: 7px; + display: grid; + place-items: center; + flex: 0 0 auto; + font-size: 13px; + font-weight: 850; +} + +.provider-logo.claude { + background: linear-gradient(135deg, #fb8947, #df5e2f); +} + +.provider-logo.codex { + background: #05070b; + border: 1px solid rgba(255,255,255,0.14); +} + +.provider-logo.gemini { + background: conic-gradient(from 45deg, #3debd4, #395fff, #ffb34d, #3debd4); +} + +.sparkline { + width: 74px; + height: 22px; +} + +.sparkline polyline { + fill: none; + stroke-width: 3; + stroke-linecap: round; + stroke-linejoin: round; +} + +.more { + color: #7e8aa0; + font-size: 20px; + text-align: center; +} + +.table-footer { + height: 38px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 22px; + color: #9ba7b8; + font-size: 14px; +} + +.table-footer button, +.insight-panel .table-title button { + color: #4b99ff; + font-weight: 700; + border: 0; + background: transparent; + padding: 0; +} + +.insight-list { + display: grid; + gap: 7px; + padding: 8px 10px 10px; +} + +.insight { + min-height: 62px; + border: 1px solid rgba(151, 174, 205, 0.15); + border-radius: 8px; + padding: 9px 12px 9px 54px; + position: relative; + display: grid; + gap: 4px; + animation: slideIn 420ms ease-out both; +} + +.insight.warn { + background: linear-gradient(90deg, rgba(255, 154, 49, 0.13), rgba(255, 154, 49, 0.04)); + border-color: rgba(255,154,49,0.24); +} + +.insight.good { + background: linear-gradient(90deg, rgba(46, 208, 110, 0.13), rgba(46, 208, 110, 0.04)); + border-color: rgba(46,208,110,0.2); +} + +.insight.spike, +.insight.info { + background: linear-gradient(90deg, rgba(155, 92, 246, 0.13), rgba(155, 92, 246, 0.04)); + border-color: rgba(155,92,246,0.22); +} + +.insight-icon { + position: absolute; + left: 14px; + top: 12px; + width: 31px; + height: 31px; + border-radius: 8px; + display: grid; + place-items: center; + font-weight: 900; + font-size: 18px; + color: var(--orange); + background: rgba(255, 154, 49, 0.16); +} + +.insight.good .insight-icon { + color: var(--green); + background: rgba(46, 208, 110, 0.14); +} + +.insight.spike .insight-icon, +.insight.info .insight-icon { + color: var(--violet); + background: rgba(155, 92, 246, 0.14); +} + +.insight-time { + position: absolute; + top: 13px; + right: 12px; + color: #94a0b3; + font-size: 12px; +} + +.insight strong { + padding-right: 36px; + font-size: 14px; + line-height: 1.35; +} + +.insight span { + color: #aab5c6; + font-size: 13px; + line-height: 1.35; +} + +.spin { + animation: spin 0.9s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +@keyframes cardIn { + from { + opacity: 0; + transform: translateY(12px) scale(0.99); + filter: blur(6px); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + filter: blur(0); + } +} + +@keyframes menuIn { + from { + opacity: 0; + transform: translateY(-6px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes drawLine { + to { + stroke-dashoffset: 0; + } +} + +@keyframes floatGlow { + 0%, 100% { + transform: translateY(0); + filter: brightness(1); + } + 50% { + transform: translateY(-2px); + filter: brightness(1.12); + } +} + +@keyframes glassPulse { + 0%, 100% { + filter: saturate(1); + } + 50% { + filter: saturate(1.18) brightness(1.05); + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(10px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/tests/displayFormat.test.mjs b/tests/displayFormat.test.mjs new file mode 100644 index 0000000..38c7748 --- /dev/null +++ b/tests/displayFormat.test.mjs @@ -0,0 +1,12 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { formatTokensZh } from "../src/renderer/displayFormat.js"; + +test("formatTokensZh uses Chinese magnitude units instead of western suffixes", () => { + assert.equal(formatTokensZh(9999), "9999"); + assert.equal(formatTokensZh(10_000), "1万"); + assert.equal(formatTokensZh(71_400), "7.1万"); + assert.equal(formatTokensZh(6_600_000), "660万"); + assert.equal(formatTokensZh(196_700_000), "1.97亿"); + assert.equal(formatTokensZh(1_020_000_000), "10.2亿"); +}); diff --git a/tests/tokenUsage.test.cjs b/tests/tokenUsage.test.cjs new file mode 100644 index 0000000..d68c672 --- /dev/null +++ b/tests/tokenUsage.test.cjs @@ -0,0 +1,182 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs/promises"); +const os = require("node:os"); +const path = require("node:path"); + +const { + parseClaudeLine, + parseCodexLine, + parseGeminiLine, + summarizeRecords, + scanUsage +} = require("../src/lib/tokenUsage.cjs"); + +test("parseClaudeLine extracts usage without reading message content", () => { + const line = JSON.stringify({ + timestamp: "2026-06-11T02:20:00.000Z", + cwd: "/Users/me/Code/App", + sessionId: "claude-session", + message: { + role: "assistant", + content: "private conversation text", + model: "claude-3-5-sonnet", + usage: { + input_tokens: 120, + output_tokens: 30, + cache_read_input_tokens: 50, + cache_creation_input_tokens: 10 + } + } + }); + + const record = parseClaudeLine(line, "/tmp/claude.jsonl"); + + assert.equal(record.provider, "Claude"); + assert.equal(record.model, "claude-3-5-sonnet"); + assert.equal(record.inputTokens, 120); + assert.equal(record.outputTokens, 30); + assert.equal(record.cachedTokens, 60); + assert.equal(record.totalTokens, 210); + assert.equal(record.contentPreview, undefined); +}); + +test("parseCodexLine uses last_token_usage instead of cumulative total usage", () => { + const line = JSON.stringify({ + timestamp: "2026-06-11T03:10:00.000Z", + type: "event_msg", + payload: { + info: { + total_token_usage: { + input_tokens: 1000, + output_tokens: 300, + cached_input_tokens: 200, + reasoning_output_tokens: 40, + total_tokens: 1300 + }, + last_token_usage: { + input_tokens: 100, + output_tokens: 30, + cached_input_tokens: 20, + reasoning_output_tokens: 4, + total_tokens: 130 + } + } + } + }); + + const record = parseCodexLine(line, "/tmp/rollout.jsonl"); + + assert.equal(record.provider, "Codex"); + assert.equal(record.inputTokens, 100); + assert.equal(record.outputTokens, 30); + assert.equal(record.cachedTokens, 20); + assert.equal(record.reasoningTokens, 4); + assert.equal(record.totalTokens, 154); +}); + +test("parseGeminiLine extracts tokens from Gemini chat jsonl", () => { + const line = JSON.stringify({ + timestamp: "2026-06-11T04:00:00.000Z", + type: "response", + model: "gemini-2.5-pro", + tokens: { + input: 70, + output: 20, + cached: 5, + thoughts: 3, + tool: 2, + total: 100 + } + }); + + const record = parseGeminiLine(line, "/tmp/session.jsonl"); + + assert.equal(record.provider, "Gemini"); + assert.equal(record.model, "gemini-2.5-pro"); + assert.equal(record.inputTokens, 70); + assert.equal(record.outputTokens, 20); + assert.equal(record.cachedTokens, 5); + assert.equal(record.reasoningTokens, 3); + assert.equal(record.toolTokens, 2); + assert.equal(record.totalTokens, 100); +}); + +test("summarizeRecords returns today, month, total, provider, trend and insight data", () => { + const now = new Date("2026-06-11T12:00:00+08:00"); + const records = [ + record("Claude", "2026-06-11T01:00:00Z", 100, 20, 10), + record("Codex", "2026-06-10T01:00:00Z", 80, 20, 0), + record("Gemini", "2026-05-31T01:00:00Z", 50, 10, 0) + ]; + + const summary = summarizeRecords(records, { now, timeZone: "Asia/Shanghai" }); + + assert.equal(summary.cards.todayTokens, 130); + assert.equal(summary.cards.monthTokens, 230); + assert.equal(summary.cards.totalTokens, 290); + assert.equal(summary.providerTotals.Claude.totalTokens, 130); + assert.equal(summary.providerTotals.Codex.totalTokens, 100); + assert.equal(summary.providerTotals.Gemini.totalTokens, 60); + assert.ok(summary.dailyTrend.length >= 2); + assert.ok(summary.sourceRows.length >= 3); + assert.ok(summary.insights.length >= 1); +}); + +test("scanUsage reads only known local usage files and aggregates them", async () => { + const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "tokenlens-")); + await fs.mkdir(path.join(homeDir, ".claude/projects/proj"), { recursive: true }); + await fs.mkdir(path.join(homeDir, ".codex/sessions/2026/06/11"), { recursive: true }); + await fs.mkdir(path.join(homeDir, ".gemini/tmp/me/chats"), { recursive: true }); + + await fs.writeFile( + path.join(homeDir, ".claude/projects/proj/a.jsonl"), + JSON.stringify({ + timestamp: "2026-06-11T02:20:00.000Z", + message: { model: "claude", usage: { input_tokens: 10, output_tokens: 5 } } + }) + "\n" + ); + await fs.writeFile( + path.join(homeDir, ".codex/sessions/2026/06/11/rollout.jsonl"), + JSON.stringify({ + timestamp: "2026-06-11T02:30:00.000Z", + payload: { info: { last_token_usage: { input_tokens: 7, output_tokens: 3 } } } + }) + "\n" + ); + await fs.writeFile( + path.join(homeDir, ".gemini/tmp/me/chats/session.jsonl"), + JSON.stringify({ + timestamp: "2026-06-11T02:40:00.000Z", + model: "gemini", + tokens: { input: 8, output: 2, total: 10 } + }) + "\n" + ); + + const summary = await scanUsage({ + homeDir, + now: new Date("2026-06-11T12:00:00+08:00"), + timeZone: "Asia/Shanghai" + }); + + assert.equal(summary.records.length, 3); + assert.equal(summary.cards.totalTokens, 35); + assert.equal(summary.providerTotals.Claude.totalTokens, 15); + assert.equal(summary.providerTotals.Codex.totalTokens, 10); + assert.equal(summary.providerTotals.Gemini.totalTokens, 10); +}); + +function record(provider, timestamp, inputTokens, outputTokens, cachedTokens) { + return { + provider, + source: `${provider} source`, + sourceFile: `/tmp/${provider}.jsonl`, + timestamp, + model: provider, + inputTokens, + outputTokens, + cachedTokens, + reasoningTokens: 0, + toolTokens: 0, + totalTokens: inputTokens + outputTokens + cachedTokens + }; +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..852fb58 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + base: "./", + build: { + outDir: "dist", + emptyOutDir: true + } +});