zframes 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -101,7 +101,7 @@ var DashboardSpecSchema = z2.preprocess(
101
101
  ),
102
102
  title: z2.string().describe("Dashboard name, like package.json's name."),
103
103
  author: z2.string().optional().describe(
104
- `Who made this dashboard \u2014 a free-form credit, like package.json's author ("Micky" or "Micky <pichaya@zentry.com>"). Optional.`
104
+ `Who made this dashboard \u2014 a free-form credit, like package.json's author ("You" or "You <you@example.com>"). Optional.`
105
105
  ),
106
106
  grid: z2.object({
107
107
  columns: z2.number().int().min(1).default(12),
@@ -154,13 +154,20 @@ var SOURCES = {
154
154
  finra: {
155
155
  name: "FINRA",
156
156
  url: "https://www.finra.org/finra-data/browse-catalog/short-sale-volume-data"
157
- }
157
+ },
158
+ ofr: {
159
+ name: "OFR",
160
+ url: "https://www.financialresearch.gov/financial-stress-index/"
161
+ },
162
+ mempool: { name: "mempool.space", url: "https://mempool.space" },
163
+ deribit: { name: "Deribit", url: "https://www.deribit.com" },
164
+ coinpaprika: { name: "Coinpaprika", url: "https://coinpaprika.com" }
158
165
  };
159
166
  var clockMeta = defineFrameMeta({
160
167
  name: "clock",
161
168
  iconUrl: widgetIcon("clock"),
162
169
  layout: { w: 3, h: 2, minW: 2, minH: 1 },
163
- description: "Digital clock showing the current time, ticking every second. Configurable IANA timezone (defaults to the viewer's local zone), 12/24-hour format, optional seconds and date, and a caption label. Drop several with different timezones for a trading-desk world clock. Needs no data provider.",
170
+ description: "Digital clock showing the current time, ticking every second. Configurable IANA timezone (defaults to the viewer's local zone), 12/24-hour format, optional seconds and date, the timezone abbreviation, and a caption label. Drop several with different timezones for a trading-desk world clock. Needs no data provider.",
164
171
  capabilities: [],
165
172
  schema: z3.object({
166
173
  timezone: z3.string().default("").describe(
@@ -174,7 +181,10 @@ var clockMeta = defineFrameMeta({
174
181
  showMillis: z3.boolean().default(false).describe(
175
182
  "Show milliseconds (HH:MM:SS.mmm), updated smoothly each animation frame. Implies seconds."
176
183
  ),
177
- showDate: z3.boolean().default(false).describe("Show the weekday and date under the time.")
184
+ showDate: z3.boolean().default(false).describe("Show the weekday and date under the time."),
185
+ showTimezone: z3.boolean().default(true).describe(
186
+ 'Show the timezone abbreviation (e.g. "EST", "GMT+7", "UTC") in the caption. Combines with the label when set, e.g. "New York \xB7 EST".'
187
+ )
178
188
  })
179
189
  });
180
190
  var marketHoursMeta = defineFrameMeta({
@@ -337,6 +347,58 @@ var inflationPulseMeta = defineFrameMeta({
337
347
  months: z3.number().int().min(13).max(36).default(18).describe("How many monthly CPI observations to show in the trend.")
338
348
  })
339
349
  });
350
+ var financialStressMeta = defineFrameMeta({
351
+ name: "financial-stress",
352
+ iconUrl: widgetIcon("financial-stress"),
353
+ layout: { w: 4, h: 3, minW: 3, minH: 2 },
354
+ description: "The OFR Financial Stress Index \u2014 a daily, market-based gauge of systemic financial stress from the U.S. Office of Financial Research. One headline value where 0 is the long-run average (positive = elevated stress, negative = calmer than normal), an optional breakdown of the five contributing categories (credit, equity valuation, safe assets, funding, volatility), and a trend line. Keyless official data, updated each business day; needs the zframes runtime's data proxy (ships with `zframes serve` / `vite dev`). Not a price feed.",
355
+ capabilities: ["financial-stress"],
356
+ source: SOURCES.ofr,
357
+ schema: z3.object({
358
+ trendDays: z3.number().int().min(20).max(90).default(60).describe("How many recent daily readings to plot in the trend line."),
359
+ showCategories: z3.boolean().default(true).describe(
360
+ "Show the five category contributions (credit, equity valuation, safe assets, funding, volatility) under the headline."
361
+ )
362
+ })
363
+ });
364
+ var nationalDebtMeta = defineFrameMeta({
365
+ name: "national-debt",
366
+ iconUrl: widgetIcon("national-debt"),
367
+ layout: { w: 4, h: 3, minW: 3, minH: 2 },
368
+ description: "U.S. total public debt outstanding from the Treasury's keyless 'Debt to the Penny' dataset \u2014 the headline total in trillions, the change over the chosen window, an optional split into debt held by the public vs intragovernmental holdings, and a trend line. Official data updated each business day; CORS-safe (no proxy needed). Macro context, not a live price feed.",
369
+ capabilities: ["national-debt"],
370
+ source: SOURCES.treasury,
371
+ schema: z3.object({
372
+ trendDays: z3.number().int().min(30).max(365).default(180).describe(
373
+ "How many business days of history to load for the trend and the change figure."
374
+ ),
375
+ showSplit: z3.boolean().default(true).describe(
376
+ "Show the debt-held-by-the-public vs intragovernmental-holdings split."
377
+ )
378
+ })
379
+ });
380
+ var laborMarketMeta = defineFrameMeta({
381
+ name: "labor-market",
382
+ iconUrl: widgetIcon("labor-market"),
383
+ layout: { w: 4, h: 3, minW: 3, minH: 2 },
384
+ description: "U.S. labor-market snapshot from the BLS keyless public API: the headline unemployment rate, the latest monthly change in nonfarm payrolls (jobs added or lost), the total payroll level, and an unemployment-rate trend line. Monthly macro context for stock dashboards; updates on the BLS jobs-report schedule, not a live feed.",
385
+ capabilities: ["macro-series"],
386
+ source: SOURCES.bls,
387
+ schema: z3.object({
388
+ months: z3.number().int().min(13).max(36).default(18).describe("How many monthly observations to show in the trend.")
389
+ })
390
+ });
391
+ var treasuryAuctionsMeta = defineFrameMeta({
392
+ name: "treasury-auctions",
393
+ iconUrl: widgetIcon("treasury-auctions"),
394
+ layout: { w: 5, h: 4, minW: 3, minH: 3 },
395
+ description: "Recent completed U.S. Treasury auctions from the keyless Fiscal Data API \u2014 each row shows the security (bill/note/bond + term), the high awarded yield, and the bid-to-cover ratio (demand: total bids \xF7 amount accepted; higher = stronger). Newest first. Official market-plumbing data, CORS-safe; updates as auctions settle, not a live price feed.",
396
+ capabilities: ["treasury-auctions"],
397
+ source: SOURCES.treasury,
398
+ schema: z3.object({
399
+ count: z3.number().int().min(3).max(20).default(8).describe("How many recent auctions to list (newest first).")
400
+ })
401
+ });
340
402
  var filingsFeedMeta = defineFrameMeta({
341
403
  name: "filings-feed",
342
404
  iconUrl: widgetIcon("filings-feed"),
@@ -416,12 +478,12 @@ var fundingHeatmapMeta = defineFrameMeta({
416
478
  name: "funding-heatmap",
417
479
  iconUrl: widgetIcon("funding-heatmap"),
418
480
  layout: { w: 6, h: 3, minW: 4, minH: 3 },
419
- description: "Heatmap of perp funding rates \u2014 symbols as rows, 4h time buckets over the last 3 days as columns, green positive / red negative. Spots persistent funding regimes at a glance.",
481
+ description: "Heatmap of perp funding rates \u2014 symbols as rows, daily average over the last 7 days as columns, green positive / red negative. Spots persistent funding regimes at a glance.",
420
482
  capabilities: ["funding-history"],
421
483
  source: SOURCES.hyperliquid,
422
484
  schema: z3.object({
423
- symbols: z3.array(z3.string()).min(1).max(8).describe(
424
- 'Hyperliquid symbols as heatmap rows, e.g. ["xyz:TSLA", "xyz:NVDA", "xyz:AAPL"].'
485
+ symbols: z3.array(z3.string()).min(2).max(8).describe(
486
+ 'Hyperliquid symbols as heatmap rows \u2014 at least 2, since the heatmap compares funding across symbols, e.g. ["xyz:TSLA", "xyz:NVDA", "xyz:AAPL"].'
425
487
  )
426
488
  })
427
489
  });
@@ -512,7 +574,383 @@ var allocationMeta = defineFrameMeta({
512
574
  ).min(2).max(8).describe("The holdings to chart. 2 to 8 positions.")
513
575
  })
514
576
  });
577
+ var newsFeedMeta = defineFrameMeta({
578
+ name: "news-feed",
579
+ layout: { w: 4, h: 4, minW: 3, minH: 3 },
580
+ description: 'Scrolling feed of the latest news headlines from a chosen outlet \u2014 each row is a clickable headline with its publish time, newest first. Free, keyless RSS sources: crypto press (CoinDesk, Cointelegraph, Decrypt), broad markets/macro (CNBC, Nasdaq), or \u2014 source "stocks" \u2014 per-company headlines (via Google News) scoped to the specific tickers in `symbols`. IMPORTANT: news feeds are CORS-blocked, so this frame reads them through the zframes runtime\'s data proxy (it ships with `zframes serve` / `vite dev`); on a fully static host with no runtime it shows an empty state.',
581
+ capabilities: ["news"],
582
+ schema: z3.object({
583
+ source: z3.enum([
584
+ "coindesk",
585
+ "cointelegraph",
586
+ "decrypt",
587
+ "cnbc",
588
+ "nasdaq",
589
+ "stocks"
590
+ ]).default("coindesk").describe(
591
+ 'Which feed to show. Crypto press: "coindesk", "cointelegraph", "decrypt". Markets/macro: "cnbc", "nasdaq". "stocks" = per-company headlines (Google News) for the tickers in `symbols`.'
592
+ ),
593
+ symbols: z3.array(z3.string()).default([]).describe(
594
+ 'Only used when source is "stocks": stock tickers to pull headlines for, e.g. ["TSLA","NVDA","AAPL"]. HIP-3 symbols ("xyz:TSLA") work too \u2014 the dex prefix is stripped. Ignored for the other sources.'
595
+ ),
596
+ count: z3.number().int().min(3).max(20).default(8).describe("How many headlines to list (newest first).")
597
+ })
598
+ });
599
+ var dexVolumeTreemapMeta = defineFrameMeta({
600
+ name: "dex-volume-treemap",
601
+ layout: { w: 6, h: 4, minW: 3, minH: 3 },
602
+ description: "Treemap of decentralized-exchange (DEX) protocols sized by trailing-24h trading volume, tiles colored green/red by 1-day change. Data from DeFiLlama. One-glance read on where on-chain trading flow is concentrated right now.",
603
+ capabilities: ["dex-volume"],
604
+ source: SOURCES.defillama,
605
+ schema: z3.object({
606
+ topN: z3.number().int().min(3).max(30).default(12).describe("How many of the highest-volume DEX protocols to show.")
607
+ })
608
+ });
609
+ var dexVolumeChartMeta = defineFrameMeta({
610
+ name: "dex-volume-chart",
611
+ layout: { w: 6, h: 3, minW: 3, minH: 2 },
612
+ description: "Multi-series line chart of daily DEX trading volume for several protocols over a lookback window \u2014 compare how Uniswap, PancakeSwap, Aerodrome etc. trend against each other. Data from DeFiLlama (daily granularity).",
613
+ capabilities: ["dex-volume"],
614
+ source: SOURCES.defillama,
615
+ schema: z3.object({
616
+ protocols: z3.array(z3.string()).min(1).max(6).describe(
617
+ 'DeFiLlama DEX protocol slugs (lowercase, hyphenated), e.g. ["uniswap", "pancakeswap", "aerodrome-slipstream"]. 1 to 6.'
618
+ ),
619
+ lookback: z3.enum(["7D", "1M", "3M"]).default("1M").describe("History window for the chart.")
620
+ })
621
+ });
622
+ var protocolTvlTreemapMeta = defineFrameMeta({
623
+ name: "protocol-tvl-treemap",
624
+ layout: { w: 6, h: 4, minW: 3, minH: 3 },
625
+ description: "Treemap of DeFi protocols sized by current total value locked (TVL), tiles colored green/red by 1-day change. Data from DeFiLlama. Unlike tvl-treemap (which groups by blockchain), this ranks individual protocols (Lido, Aave, EigenLayer\u2026).",
626
+ capabilities: ["protocol-tvl"],
627
+ source: SOURCES.defillama,
628
+ schema: z3.object({
629
+ topN: z3.number().int().min(3).max(30).default(12).describe("How many of the largest protocols by TVL to show.")
630
+ })
631
+ });
632
+ var protocolTvlChartMeta = defineFrameMeta({
633
+ name: "protocol-tvl-chart",
634
+ layout: { w: 6, h: 3, minW: 3, minH: 2 },
635
+ description: "Multi-series line chart of total value locked (TVL) for several DeFi protocols over a lookback window. Data from DeFiLlama (daily granularity).",
636
+ capabilities: ["protocol-tvl"],
637
+ source: SOURCES.defillama,
638
+ schema: z3.object({
639
+ protocols: z3.array(z3.string()).min(1).max(6).describe(
640
+ 'DeFiLlama protocol slugs (lowercase, hyphenated), e.g. ["lido", "aave", "eigenlayer"]. 1 to 6.'
641
+ ),
642
+ lookback: z3.enum(["7D", "1M", "3M"]).default("1M").describe("History window for the chart.")
643
+ })
644
+ });
645
+ var protocolFeesTreemapMeta = defineFrameMeta({
646
+ name: "protocol-fees-treemap",
647
+ layout: { w: 6, h: 4, minW: 3, minH: 3 },
648
+ description: "Treemap of protocols sized by the fees they generated in the last 24h, tiles colored green/red by 1-day change. Data from DeFiLlama. Shows where on-chain users are actually paying for blockspace and services right now.",
649
+ capabilities: ["protocol-fees"],
650
+ source: SOURCES.defillama,
651
+ schema: z3.object({
652
+ topN: z3.number().int().min(3).max(30).default(12).describe("How many of the highest fee-earning protocols to show.")
653
+ })
654
+ });
655
+ var marketCapTreemapMeta = defineFrameMeta({
656
+ name: "market-cap-treemap",
657
+ layout: { w: 6, h: 4, minW: 3, minH: 3 },
658
+ description: "Treemap of the largest cryptocurrencies sized by market capitalisation, tiles colored green/red by 24h price change. Data from CoinGecko (free tier). A heat-map of the whole crypto market at a glance.",
659
+ capabilities: ["coin-markets"],
660
+ source: SOURCES.coingecko,
661
+ schema: z3.object({
662
+ topN: z3.number().int().min(5).max(50).default(20).describe(
663
+ "How many of the largest coins by market cap to show (up to 50)."
664
+ )
665
+ })
666
+ });
667
+ var openInterestMeta = defineFrameMeta({
668
+ name: "open-interest",
669
+ layout: { w: 4, h: 3, minW: 3, minH: 2 },
670
+ description: 'Live open interest across a watchlist of Hyperliquid perps \u2014 each symbol is a horizontal bar sized by USD notional, largest first, refreshed on a ~30s poll. Single-venue (Hyperliquid only), so read it as a relative gauge across your symbols, not a market-wide total. Stocks (HIP-3, e.g. "xyz:TSLA") and crypto both work.',
671
+ capabilities: ["open-interest"],
672
+ source: SOURCES.hyperliquid,
673
+ schema: z3.object({
674
+ symbols: z3.array(z3.string()).min(1).max(20).describe(
675
+ `Hyperliquid symbols to compare open interest for, e.g. ["BTC", "ETH", "xyz:TSLA"]. A "<dex>:*" wildcard (e.g. "xyz:*") pulls that dex's entire universe.`
676
+ )
677
+ })
678
+ });
679
+ var snakeMeta = defineFrameMeta({
680
+ name: "snake",
681
+ layout: { w: 4, h: 4, minW: 3, minH: 3 },
682
+ description: "Classic snake game on canvas \u2014 steer with the arrow keys (or swipe), eat dots to grow, avoid the walls and your own tail. High score persists locally. For when the market is flat. Needs no data provider.",
683
+ capabilities: [],
684
+ schema: z3.object({})
685
+ });
686
+ var flappyBirdMeta = defineFrameMeta({
687
+ name: "flappy-bird",
688
+ layout: { w: 4, h: 4, minW: 3, minH: 3 },
689
+ description: "Flappy-bird style game on canvas \u2014 tap or press SPACE to flap through the gaps between pipes. High score persists locally. Needs no data provider.",
690
+ capabilities: [],
691
+ schema: z3.object({})
692
+ });
693
+ var videoMeta = defineFrameMeta({
694
+ name: "video",
695
+ layout: { w: 4, h: 3, minW: 2, minH: 2 },
696
+ description: "Embeds a video from a YouTube or Vimeo link (or any direct embed URL) as an iframe \u2014 a livestream, a market-news clip, a focus playlist. Needs no data provider.",
697
+ capabilities: [],
698
+ schema: z3.object({
699
+ url: z3.string().min(1).describe(
700
+ "Video URL \u2014 a YouTube watch/share link, a Vimeo link, or a direct embeddable URL (https)."
701
+ ),
702
+ title: z3.string().default("Video").describe("Accessible title for the embedded player (iframe title).")
703
+ })
704
+ });
705
+ var drawdyMeta = defineFrameMeta({
706
+ name: "drawdy",
707
+ layout: { w: 8, h: 6, minW: 2, minH: 2 },
708
+ description: "Embeds drawdy.io as an interactive whiteboard canvas. No configuration needed.",
709
+ capabilities: [],
710
+ schema: z3.object({})
711
+ });
712
+ var countdownMeta = defineFrameMeta({
713
+ name: "countdown",
714
+ layout: { w: 3, h: 2, minW: 2, minH: 1 },
715
+ description: "Live countdown to a target date and time \u2014 FOMC decisions, CPI prints, options expiry, earnings, a token unlock, the next market open. Counts down in days / hours / minutes / seconds, ticking every second, and flips to a 'reached' state once the moment passes. Needs no data provider.",
716
+ capabilities: [],
717
+ schema: z3.object({
718
+ target: z3.string().default("").describe(
719
+ `The moment to count down to, as an ISO 8601 string. Add a timezone for an unambiguous instant, e.g. "2026-07-30T18:00:00-04:00" or "2026-12-31T23:59:59Z"; a bare "2026-07-30T18:00" is read in the viewer's local timezone. Empty shows a "set a target" prompt.`
720
+ ),
721
+ label: z3.string().default("").describe(
722
+ 'Caption above the countdown, e.g. "FOMC Decision". Empty hides it.'
723
+ ),
724
+ showTarget: z3.boolean().default(true).describe("Show the formatted target date and time under the countdown.")
725
+ })
726
+ });
727
+ var linkGridMeta = defineFrameMeta({
728
+ name: "link-grid",
729
+ layout: { w: 3, h: 2, minW: 2, minH: 1 },
730
+ description: "A grid of quick-launch tiles linking to your favourite sites \u2014 TradingView, exchanges, news, docs, your own dashboards. Each tile opens in a new tab and shows the destination site's favicon by default (fetched keyless from a public favicon service), with an optional per-link icon override and a first-letter fallback. Needs no data provider.",
731
+ capabilities: [],
732
+ schema: z3.object({
733
+ links: z3.array(
734
+ z3.object({
735
+ label: z3.string().min(1).describe('Tile caption, e.g. "TradingView".'),
736
+ url: z3.string().min(1).describe("Destination URL (https). Opens in a new tab."),
737
+ icon: z3.string().default("").describe(
738
+ `Optional icon override: an emoji (e.g. "\u{1F4C8}") or an https image URL. Empty uses the destination site's favicon, falling back to the label's first letter.`
739
+ )
740
+ })
741
+ ).min(1).default([
742
+ {
743
+ label: "TradingView",
744
+ url: "https://www.tradingview.com",
745
+ icon: "\u{1F4C8}"
746
+ },
747
+ {
748
+ label: "Hyperliquid",
749
+ url: "https://app.hyperliquid.xyz",
750
+ icon: "\u26A1"
751
+ }
752
+ ]).describe("The links to show as tiles. At least one."),
753
+ columns: z3.number().int().min(1).max(4).default(2).describe("How many tiles per row.")
754
+ })
755
+ });
756
+ var calculatorMeta = defineFrameMeta({
757
+ name: "calculator",
758
+ layout: { w: 3, h: 3, minW: 2, minH: 2 },
759
+ description: "Position-size & risk calculator. Enter account size, risk-per-trade %, entry and stop price; it computes the dollars at risk, the per-unit risk, the position size (units) that respects that risk budget, the resulting position value, and whether the setup is long or short. All math runs client-side \u2014 no data provider. Inputs are editable live; the configured values are the starting point.",
760
+ capabilities: [],
761
+ schema: z3.object({
762
+ account: z3.number().positive().default(1e4).describe("Account size used as the risk base, in the quote currency."),
763
+ riskPct: z3.number().positive().max(100).default(1).describe("Percent of the account risked on the trade, e.g. 1 = 1%."),
764
+ entry: z3.number().positive().default(100).describe("Entry price."),
765
+ stop: z3.number().positive().default(95).describe(
766
+ "Stop-loss price. Its distance from entry sets the per-unit risk; below entry = long, above = short."
767
+ ),
768
+ currency: z3.string().default("$").describe("Currency symbol shown next to money values.")
769
+ })
770
+ });
771
+ var quoteMeta = defineFrameMeta({
772
+ name: "quote",
773
+ layout: { w: 4, h: 2, minW: 2, minH: 1 },
774
+ description: 'Displays a market or trading quote, centered \u2014 set one or rotate through several. A calm bit of wall-art for the dashboard: trading maxims, reminders of your own rules, mantras. Write any attribution into the text itself (e.g. "\u2026 \u2014 Buffett"). Needs no data provider.',
775
+ capabilities: [],
776
+ schema: z3.object({
777
+ quotes: z3.array(z3.string().min(1)).min(1).default([
778
+ "Be fearful when others are greedy, and greedy when others are fearful. \u2014 Warren Buffett",
779
+ "The trend is your friend until the end when it bends.",
780
+ "Plan the trade, trade the plan."
781
+ ]).describe(
782
+ "One or more quotes. With more than one, the frame rotates through them."
783
+ ),
784
+ intervalSec: z3.number().int().min(0).default(12).describe(
785
+ "Seconds between rotations when there are multiple quotes. 0 shows the first quote, fixed."
786
+ )
787
+ })
788
+ });
789
+ var dividerMeta = defineFrameMeta({
790
+ name: "divider",
791
+ layout: { w: 12, h: 1, minW: 1, minH: 1 },
792
+ description: "A plain rule that separates regions of the dashboard, with an optional centered label. Renders chrome-less (no card) \u2014 lighter than a heading. Use a horizontal divider full-width between stacked zones, or set orientation to vertical for a 1-column-wide column separator. Needs no data provider.",
793
+ capabilities: [],
794
+ chrome: "bare",
795
+ schema: z3.object({
796
+ label: z3.string().default("").describe(
797
+ "Optional text shown in the middle of the rule. Empty = a clean line."
798
+ ),
799
+ orientation: z3.enum(["horizontal", "vertical"]).default("horizontal").describe(
800
+ "Horizontal rule (spans the width) or vertical rule (spans the height)."
801
+ ),
802
+ style: z3.enum(["solid", "dashed", "dotted"]).default("solid").describe("Line style.")
803
+ })
804
+ });
805
+ var btcFeesMeta = defineFrameMeta({
806
+ name: "btc-fees",
807
+ iconUrl: widgetIcon("btc-fees"),
808
+ layout: { w: 3, h: 2, minW: 2, minH: 2 },
809
+ description: "Recommended Bitcoin on-chain fee rates (sat/vB) from mempool.space \u2014 the next-block ('fastest'), ~30-minute, ~1-hour, economy, and minimum tiers, as a compact gauge. Live mempool data, keyless; updates every ~30s.",
810
+ capabilities: ["btc-fees"],
811
+ source: SOURCES.mempool,
812
+ schema: z3.object({
813
+ tiers: z3.array(z3.enum(["fastest", "halfHour", "hour", "economy", "minimum"])).min(1).max(5).default(["fastest", "halfHour", "hour", "economy"]).describe(
814
+ 'Which fee tiers to show, in order. "fastest" = next block, "halfHour"/"hour" = within ~30/60 min, "economy"/"minimum" = cheapest relayable.'
815
+ )
816
+ })
817
+ });
818
+ var btcMempoolMeta = defineFrameMeta({
819
+ name: "btc-mempool",
820
+ iconUrl: widgetIcon("btc-mempool"),
821
+ layout: { w: 5, h: 3, minW: 3, minH: 2 },
822
+ description: "Bitcoin mempool congestion at a glance \u2014 unconfirmed transaction count, total pending vsize, and a row of projected ('template') blocks the network will likely mine next, each labelled with its median fee rate (sat/vB) and tx count. Live mempool data from mempool.space, keyless.",
823
+ capabilities: ["btc-mempool"],
824
+ source: SOURCES.mempool,
825
+ schema: z3.object({
826
+ projectedBlocks: z3.number().int().min(1).max(8).default(5).describe(
827
+ "How many projected (yet-to-be-mined) blocks to show, next-to-mine first."
828
+ )
829
+ })
830
+ });
831
+ var btcBlocksMeta = defineFrameMeta({
832
+ name: "btc-blocks",
833
+ iconUrl: widgetIcon("btc-blocks"),
834
+ layout: { w: 5, h: 4, minW: 3, minH: 3 },
835
+ description: "Feed of the most recently mined Bitcoin blocks \u2014 each row shows the height, how long ago it was mined, transaction count, the mining pool that found it, total fees (BTC), and size. Live data from mempool.space, keyless; newest first.",
836
+ capabilities: ["btc-blocks"],
837
+ source: SOURCES.mempool,
838
+ schema: z3.object({
839
+ count: z3.number().int().min(3).max(15).default(8).describe("How many recent blocks to list (newest first).")
840
+ })
841
+ });
842
+ var btcHashrateMeta = defineFrameMeta({
843
+ name: "btc-hashrate",
844
+ iconUrl: widgetIcon("btc-hashrate"),
845
+ layout: { w: 6, h: 3, minW: 3, minH: 2 },
846
+ description: "Bitcoin network hashrate over time as a line chart, with the current hashrate (EH/s) and difficulty as headline figures. Shows the long-run security trend of the network. Data from mempool.space (daily granularity), keyless.",
847
+ capabilities: ["btc-hashrate"],
848
+ source: SOURCES.mempool,
849
+ schema: z3.object({
850
+ window: z3.enum(["1y", "2y", "3y"]).default("1y").describe("History window for the hashrate line.")
851
+ })
852
+ });
853
+ var btcDifficultyMeta = defineFrameMeta({
854
+ name: "btc-difficulty",
855
+ iconUrl: widgetIcon("btc-difficulty"),
856
+ layout: { w: 4, h: 3, minW: 3, minH: 2 },
857
+ description: "Countdown to the next Bitcoin difficulty adjustment \u2014 a progress bar through the current 2016-block epoch, the estimated change (+ = mining gets harder), blocks remaining, and the estimated retarget date. Also shows the previous adjustment. Data from mempool.space, keyless.",
858
+ capabilities: ["btc-difficulty"],
859
+ source: SOURCES.mempool,
860
+ schema: z3.object({
861
+ showPrevious: z3.boolean().default(true).describe(
862
+ "Also show the percentage change applied at the previous retarget."
863
+ )
864
+ })
865
+ });
866
+ var miningPoolsMeta = defineFrameMeta({
867
+ name: "mining-pools",
868
+ iconUrl: widgetIcon("mining-pools"),
869
+ layout: { w: 6, h: 4, minW: 3, minH: 3 },
870
+ description: "Treemap of Bitcoin mining-pool dominance over a window \u2014 each tile is a pool sized by the share of blocks it mined, so you can see how concentrated hashpower is right now (Foundry, AntPool, ViaBTC\u2026). Data from mempool.space, keyless.",
871
+ capabilities: ["mining-pools"],
872
+ source: SOURCES.mempool,
873
+ schema: z3.object({
874
+ window: z3.enum(["24h", "3d", "1w", "1m"]).default("1w").describe("Window over which to measure each pool's block share."),
875
+ topN: z3.number().int().min(3).max(25).default(12).describe(
876
+ "How many of the largest pools to show; the rest fold into 'Other'."
877
+ )
878
+ })
879
+ });
880
+ var lightningStatsMeta = defineFrameMeta({
881
+ name: "lightning-stats",
882
+ iconUrl: widgetIcon("lightning-stats"),
883
+ layout: { w: 4, h: 3, minW: 3, minH: 2 },
884
+ description: "Bitcoin Lightning Network snapshot \u2014 public node count, channel count, and total network capacity (BTC), with a day-over-day delta and the Tor/clearnet node split. Data from mempool.space, keyless; updates roughly daily.",
885
+ capabilities: ["lightning-stats"],
886
+ source: SOURCES.mempool,
887
+ schema: z3.object({
888
+ showSplit: z3.boolean().default(true).describe(
889
+ "Show the Tor vs clearnet node split under the headline stats."
890
+ )
891
+ })
892
+ });
893
+ var optionsPutCallMeta = defineFrameMeta({
894
+ name: "options-put-call",
895
+ iconUrl: widgetIcon("options-put-call"),
896
+ layout: { w: 4, h: 3, minW: 3, minH: 2 },
897
+ description: "Deribit options put/call ratio for BTC or ETH \u2014 the headline ratio (by open interest or 24h volume), a call-vs-put open-interest split bar, and the open-interest-weighted average implied volatility. A ratio above 1 means puts outweigh calls (defensive positioning). Keyless Deribit market data.",
898
+ capabilities: ["options-summary"],
899
+ source: SOURCES.deribit,
900
+ schema: z3.object({
901
+ currency: z3.enum(["BTC", "ETH"]).default("BTC").describe(
902
+ "Which Deribit options book to summarise \u2014 BTC or ETH (the only deeply liquid books)."
903
+ ),
904
+ basis: z3.enum(["oi", "volume"]).default("oi").describe(
905
+ 'Headline put/call ratio basis: "oi" = by open interest (positioning), "volume" = by 24h traded volume (flow). The other is shown smaller.'
906
+ )
907
+ })
908
+ });
909
+ var optionsIvMeta = defineFrameMeta({
910
+ name: "options-iv",
911
+ iconUrl: widgetIcon("options-iv"),
912
+ layout: { w: 6, h: 3, minW: 3, minH: 2 },
913
+ description: "Deribit DVOL implied-volatility index for BTC or ETH over time \u2014 the crypto equivalent of the VIX, as a line chart with the current reading and its change over the window. Rising DVOL = the market is pricing bigger expected swings. Keyless Deribit market data.",
914
+ capabilities: ["volatility-index"],
915
+ source: SOURCES.deribit,
916
+ schema: z3.object({
917
+ currency: z3.enum(["BTC", "ETH"]).default("BTC").describe("Which DVOL volatility index to plot \u2014 BTC or ETH."),
918
+ lookback: z3.enum(["7D", "1M", "3M"]).default("1M").describe("History window for the volatility-index line.")
919
+ })
920
+ });
921
+ var optionsOiStrikeMeta = defineFrameMeta({
922
+ name: "options-oi-strike",
923
+ iconUrl: widgetIcon("options-oi-strike"),
924
+ layout: { w: 6, h: 4, minW: 4, minH: 3 },
925
+ description: "Open interest by strike for the nearest Deribit options expiry (BTC or ETH) \u2014 a grouped histogram of call vs put open interest across strikes, with the current spot marked. Surfaces the strike 'walls' where positioning is concentrated. Keyless Deribit market data.",
926
+ capabilities: ["options-summary"],
927
+ source: SOURCES.deribit,
928
+ schema: z3.object({
929
+ currency: z3.enum(["BTC", "ETH"]).default("BTC").describe("Which Deribit options book \u2014 BTC or ETH."),
930
+ strikes: z3.number().int().min(6).max(30).default(14).describe(
931
+ "How many strikes nearest the current spot to show (centered on the underlying price)."
932
+ )
933
+ })
934
+ });
935
+ var coinMoversMeta = defineFrameMeta({
936
+ name: "coin-movers",
937
+ iconUrl: widgetIcon("coin-movers"),
938
+ layout: { w: 5, h: 4, minW: 3, minH: 3 },
939
+ description: "Broad-market crypto gainers and losers across the top ~300 coins by market cap, over a selectable window (1h / 24h / 7d / 30d) \u2014 the biggest movers side by side with price and % change. Unlike a top-coins heatmap, this surfaces mid- and small-caps that ripped or dumped, not just the megacaps. Keyless data from Coinpaprika. Crypto only.",
940
+ capabilities: ["coin-movers"],
941
+ source: SOURCES.coinpaprika,
942
+ schema: z3.object({
943
+ window: z3.enum(["1h", "24h", "7d", "30d"]).default("24h").describe(
944
+ "Which price-change window ranks the movers: 1h (intraday momentum), 24h (daily), 7d (weekly), 30d (monthly)."
945
+ ),
946
+ count: z3.number().int().min(3).max(15).default(6).describe("How many gainers and how many losers to list (each side)."),
947
+ minRank: z3.number().int().min(20).max(300).default(150).describe(
948
+ "Only consider coins ranked at or above this market-cap rank \u2014 a liquidity floor that keeps illiquid micro-cap dust (which posts absurd % moves on no volume) out of the list. Lower = stricter (megacaps only); higher = includes more small-caps."
949
+ )
950
+ })
951
+ });
515
952
  var frameMetas = [
953
+ newsFeedMeta,
516
954
  allocationMeta,
517
955
  bitcoinDominanceMeta,
518
956
  clockMeta,
@@ -521,6 +959,10 @@ var frameMetas = [
521
959
  fearGreedMeta,
522
960
  filingsFeedMeta,
523
961
  fundamentalsMeta,
962
+ financialStressMeta,
963
+ laborMarketMeta,
964
+ nationalDebtMeta,
965
+ treasuryAuctionsMeta,
524
966
  fundingHeatmapMeta,
525
967
  fundingRateChartMeta,
526
968
  headingMeta,
@@ -536,7 +978,34 @@ var frameMetas = [
536
978
  shortVolumeMeta,
537
979
  topMoversMeta,
538
980
  tvlTreemapMeta,
539
- yieldCurveMeta
981
+ yieldCurveMeta,
982
+ dexVolumeTreemapMeta,
983
+ dexVolumeChartMeta,
984
+ protocolTvlTreemapMeta,
985
+ protocolTvlChartMeta,
986
+ protocolFeesTreemapMeta,
987
+ marketCapTreemapMeta,
988
+ openInterestMeta,
989
+ snakeMeta,
990
+ flappyBirdMeta,
991
+ videoMeta,
992
+ drawdyMeta,
993
+ countdownMeta,
994
+ linkGridMeta,
995
+ calculatorMeta,
996
+ quoteMeta,
997
+ dividerMeta,
998
+ btcFeesMeta,
999
+ btcMempoolMeta,
1000
+ btcBlocksMeta,
1001
+ btcHashrateMeta,
1002
+ btcDifficultyMeta,
1003
+ miningPoolsMeta,
1004
+ lightningStatsMeta,
1005
+ optionsPutCallMeta,
1006
+ optionsIvMeta,
1007
+ optionsOiStrikeMeta,
1008
+ coinMoversMeta
540
1009
  ];
541
1010
 
542
1011
  // src/init.ts
@@ -582,7 +1051,7 @@ function skeleton(title, author) {
582
1051
  background: {
583
1052
  type: "unicorn",
584
1053
  projectId: "YrTzGatwjK7EoFpCSfgZ",
585
- opacity: 0.05
1054
+ opacity: 1
586
1055
  },
587
1056
  theme: { accentHue: 242, accentSat: 90 },
588
1057
  appearance: {
@@ -643,17 +1112,76 @@ function init(args) {
643
1112
  return 0;
644
1113
  }
645
1114
 
1115
+ // src/lint.ts
1116
+ function lintSpec(spec) {
1117
+ const issues = [];
1118
+ const metaByName = new Map(frameMetas.map((meta) => [meta.name, meta]));
1119
+ const seenIds = /* @__PURE__ */ new Set();
1120
+ for (const instance of spec.frames) {
1121
+ if (seenIds.has(instance.id))
1122
+ issues.push({
1123
+ frameId: instance.id,
1124
+ message: `duplicate frame id "${instance.id}"`
1125
+ });
1126
+ seenIds.add(instance.id);
1127
+ const meta = metaByName.get(instance.frame);
1128
+ if (!meta) {
1129
+ issues.push({
1130
+ frameId: instance.id,
1131
+ message: `unknown frame "${instance.frame}". available: ${[
1132
+ ...metaByName.keys()
1133
+ ].join(", ")}`
1134
+ });
1135
+ continue;
1136
+ }
1137
+ const parsed = meta.schema.safeParse(instance.config);
1138
+ if (!parsed.success) {
1139
+ for (const issue of parsed.error.issues) {
1140
+ issues.push({
1141
+ frameId: instance.id,
1142
+ message: `config.${issue.path.join(".") || "(root)"}: ${issue.message}`
1143
+ });
1144
+ }
1145
+ }
1146
+ if (instance.position.x + instance.position.w > spec.grid.columns)
1147
+ issues.push({
1148
+ frameId: instance.id,
1149
+ message: `overflows the grid: x(${instance.position.x}) + w(${instance.position.w}) > ${spec.grid.columns} columns`
1150
+ });
1151
+ }
1152
+ for (let i = 0; i < spec.frames.length; i++) {
1153
+ for (let j = i + 1; j < spec.frames.length; j++) {
1154
+ const a = spec.frames[i].position;
1155
+ const b = spec.frames[j].position;
1156
+ const overlap = a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
1157
+ if (overlap)
1158
+ issues.push({
1159
+ frameId: spec.frames[i].id,
1160
+ message: `overlaps frame "${spec.frames[j].id}"`
1161
+ });
1162
+ }
1163
+ }
1164
+ return issues;
1165
+ }
1166
+
646
1167
  // src/serve.ts
647
- import { createReadStream, existsSync as existsSync2, statSync as statSync2 } from "fs";
1168
+ import { existsSync as existsSync2, statSync as statSync2 } from "fs";
648
1169
  import { createServer } from "http";
649
- import { extname, join as join3, resolve as resolve2, sep } from "path";
1170
+ import { join as join3, resolve as resolve2 } from "path";
650
1171
  import { fileURLToPath } from "url";
1172
+ import sirv from "sirv";
651
1173
 
652
1174
  // ../core/src/serve.ts
653
1175
  import { readFile, writeFile } from "fs/promises";
1176
+
1177
+ // ../core/src/routes.ts
654
1178
  var DASHBOARD_READ_ROUTE = "/__zframes/dashboard.json";
655
1179
  var DASHBOARD_WRITE_ROUTE = "/__zframes/dashboard";
656
1180
  var DASHBOARD_PROXY_ROUTE = "/__zframes/proxy";
1181
+ var AGENTS_LIST_ROUTE = "/__zframes/agents";
1182
+ var ASK_ROUTE = "/__zframes/ask";
1183
+
1184
+ // ../core/src/serve.ts
657
1185
  var MAX_BODY_BYTES = 2e6;
658
1186
  var PROXY_ALLOW_HOSTS = /* @__PURE__ */ new Set([
659
1187
  "data.sec.gov",
@@ -666,7 +1194,15 @@ var PROXY_ALLOW_HOSTS = /* @__PURE__ */ new Set([
666
1194
  "markets.newyorkfed.org",
667
1195
  "api.fiscaldata.treasury.gov",
668
1196
  "api.bls.gov",
669
- "cdn.finra.org"
1197
+ "cdn.finra.org",
1198
+ // News-outlet RSS feeds (CORS-blocked, so the news-feed frame reads them
1199
+ // through here). Headlines + links only; no keys.
1200
+ "www.coindesk.com",
1201
+ "cointelegraph.com",
1202
+ "decrypt.co",
1203
+ "www.cnbc.com",
1204
+ "www.nasdaq.com",
1205
+ "news.google.com"
670
1206
  ]);
671
1207
  var PROXY_MAX_BYTES = 16e6;
672
1208
  var PROXY_TIMEOUT_MS = 2e4;
@@ -780,8 +1316,6 @@ import { access, readFile as readFile2 } from "fs/promises";
780
1316
  import { constants } from "fs";
781
1317
  import { tmpdir } from "os";
782
1318
  import { dirname as dirname2, join as join2 } from "path";
783
- var AGENTS_LIST_ROUTE = "/__zframes/agents";
784
- var ASK_ROUTE = "/__zframes/ask";
785
1319
  var MAX_BODY_BYTES2 = 64e3;
786
1320
  var RUN_TIMEOUT_MS = 12e4;
787
1321
  var RUNNERS = [
@@ -987,23 +1521,6 @@ function handleAsk(req, res, specFile) {
987
1521
 
988
1522
  // src/serve.ts
989
1523
  var DEFAULT_PORT = 37263;
990
- var MIME = {
991
- ".html": "text/html; charset=utf-8",
992
- ".js": "text/javascript; charset=utf-8",
993
- ".mjs": "text/javascript; charset=utf-8",
994
- ".css": "text/css; charset=utf-8",
995
- ".json": "application/json; charset=utf-8",
996
- ".map": "application/json; charset=utf-8",
997
- ".svg": "image/svg+xml",
998
- ".png": "image/png",
999
- ".jpg": "image/jpeg",
1000
- ".jpeg": "image/jpeg",
1001
- ".gif": "image/gif",
1002
- ".webp": "image/webp",
1003
- ".ico": "image/x-icon",
1004
- ".woff2": "font/woff2",
1005
- ".txt": "text/plain; charset=utf-8"
1006
- };
1007
1524
  function parseArgs2(args) {
1008
1525
  let file = "dashboard.json";
1009
1526
  let port = DEFAULT_PORT;
@@ -1029,31 +1546,6 @@ function parseArgs2(args) {
1029
1546
  }
1030
1547
  return { file, port, contact };
1031
1548
  }
1032
- function resolveWithin(rootDir, decodedPath) {
1033
- const rel = decodedPath === "/" ? "/index.html" : decodedPath;
1034
- const abs = resolve2(rootDir, `.${rel}`);
1035
- if (abs !== rootDir && !abs.startsWith(rootDir + sep)) return null;
1036
- return abs;
1037
- }
1038
- function sendFile(absPath, res) {
1039
- res.statusCode = 200;
1040
- res.setHeader(
1041
- "content-type",
1042
- MIME[extname(absPath).toLowerCase()] ?? "application/octet-stream"
1043
- );
1044
- createReadStream(absPath).pipe(res);
1045
- }
1046
- function tryStatic(rootDir, decodedPath, res) {
1047
- const abs = resolveWithin(rootDir, decodedPath);
1048
- if (!abs) return false;
1049
- try {
1050
- if (!statSync2(abs).isFile()) return false;
1051
- } catch {
1052
- return false;
1053
- }
1054
- sendFile(abs, res);
1055
- return true;
1056
- }
1057
1549
  function serve(args) {
1058
1550
  const parsed = parseArgs2(args);
1059
1551
  if ("error" in parsed) {
@@ -1076,6 +1568,9 @@ function serve(args) {
1076
1568
  console.error(" run `pnpm build:cli` to build it.");
1077
1569
  return Promise.resolve(1);
1078
1570
  }
1571
+ const serveBundle = sirv(bundleDir, { dev: true });
1572
+ const serveSiblings = sirv(userDir, { dev: true });
1573
+ const serveSpa = sirv(bundleDir, { dev: true, single: true });
1079
1574
  return new Promise((done) => {
1080
1575
  const server = createServer((req, res) => {
1081
1576
  const rawPath = (req.url ?? "/").split("?")[0];
@@ -1129,9 +1624,18 @@ function serve(args) {
1129
1624
  res.end();
1130
1625
  return;
1131
1626
  }
1132
- if (tryStatic(bundleDir, path, res)) return;
1133
- if (path !== "/" && tryStatic(userDir, path, res)) return;
1134
- sendFile(join3(bundleDir, "index.html"), res);
1627
+ serveBundle(
1628
+ req,
1629
+ res,
1630
+ () => serveSiblings(
1631
+ req,
1632
+ res,
1633
+ () => serveSpa(req, res, () => {
1634
+ res.statusCode = 404;
1635
+ res.end();
1636
+ })
1637
+ )
1638
+ );
1135
1639
  });
1136
1640
  server.on("error", (err) => {
1137
1641
  if (err.code === "EADDRINUSE") {
@@ -1144,7 +1648,7 @@ function serve(args) {
1144
1648
  done(1);
1145
1649
  });
1146
1650
  server.listen(parsed.port, "127.0.0.1", () => {
1147
- const url = `http://127.0.0.1:${parsed.port}`;
1651
+ const url = `http://localhost:${parsed.port}`;
1148
1652
  console.log(`\u26A1 zframes is live at ${url}`);
1149
1653
  console.log(
1150
1654
  ` serving ${parsed.file} \u2014 live editing on; drag, resize, then Save writes back.`
@@ -1157,107 +1661,308 @@ function serve(args) {
1157
1661
  import { existsSync as existsSync3, readFileSync } from "fs";
1158
1662
  import { dirname as dirname3, resolve as resolve3 } from "path";
1159
1663
 
1664
+ // ../core/src/cache.ts
1665
+ var TtlCache = class {
1666
+ entries = /* @__PURE__ */ new Map();
1667
+ inflight = /* @__PURE__ */ new Map();
1668
+ namespace;
1669
+ ttlMs;
1670
+ persist;
1671
+ staleOnError;
1672
+ revive;
1673
+ constructor(options) {
1674
+ this.namespace = options.namespace;
1675
+ this.ttlMs = options.ttlMs;
1676
+ this.persist = options.persist ?? false;
1677
+ this.staleOnError = options.staleOnError ?? true;
1678
+ this.revive = options.revive;
1679
+ }
1680
+ /**
1681
+ * Resolve the value for `key`, calling `load` only on a miss or a stale entry.
1682
+ * Serves a fresh cached value with no network call, coalesces concurrent loads
1683
+ * onto one promise, and (by default) serves the last good value if `load` throws.
1684
+ */
1685
+ get(key, load) {
1686
+ const entry = this.read(key);
1687
+ if (entry && Date.now() - entry.at < this.ttlMs)
1688
+ return Promise.resolve(entry.value);
1689
+ const pending = this.inflight.get(key);
1690
+ if (pending) return pending;
1691
+ const promise = load().then((value) => {
1692
+ this.write(key, value);
1693
+ return value;
1694
+ }).catch((error) => {
1695
+ if (this.staleOnError && entry) return entry.value;
1696
+ throw error;
1697
+ }).finally(() => {
1698
+ this.inflight.delete(key);
1699
+ });
1700
+ this.inflight.set(key, promise);
1701
+ return promise;
1702
+ }
1703
+ /** Hydrate from memo, falling back to localStorage on a cold (persisted) read. */
1704
+ read(key) {
1705
+ const memo = this.entries.get(key);
1706
+ if (memo) return memo;
1707
+ if (!this.persist || typeof localStorage === "undefined") return null;
1708
+ try {
1709
+ const raw = localStorage.getItem(this.storageKey(key));
1710
+ if (!raw) return null;
1711
+ const parsed = JSON.parse(raw);
1712
+ if (typeof parsed?.at !== "number") return null;
1713
+ let value;
1714
+ if (this.revive) {
1715
+ const revived = this.revive(parsed.value);
1716
+ if (revived === null) return null;
1717
+ value = revived;
1718
+ } else {
1719
+ value = parsed.value;
1720
+ }
1721
+ const entry = { at: parsed.at, value };
1722
+ this.entries.set(key, entry);
1723
+ return entry;
1724
+ } catch {
1725
+ return null;
1726
+ }
1727
+ }
1728
+ write(key, value) {
1729
+ const entry = { at: Date.now(), value };
1730
+ this.entries.set(key, entry);
1731
+ if (!this.persist || typeof localStorage === "undefined") return;
1732
+ try {
1733
+ localStorage.setItem(this.storageKey(key), JSON.stringify(entry));
1734
+ } catch {
1735
+ }
1736
+ }
1737
+ storageKey(key) {
1738
+ return `${this.namespace}:${key}`;
1739
+ }
1740
+ };
1741
+
1160
1742
  // ../core/src/fetch.ts
1161
1743
  var DEFAULT_TIMEOUT_MS = 1e4;
1162
1744
  var USER_AGENT = "zframes (+https://github.com/zentryhq/zframes)";
1163
- var PROXY_ROUTE = "/__zframes/proxy";
1164
- async function fetchJson(url, schema, { timeoutMs = DEFAULT_TIMEOUT_MS, init: init2, proxied } = {}) {
1745
+ async function request(url, { timeoutMs = DEFAULT_TIMEOUT_MS, init: init2, proxied }) {
1165
1746
  const headers = new Headers(init2?.headers);
1166
1747
  if (typeof document === "undefined" && !headers.has("User-Agent")) {
1167
1748
  headers.set("User-Agent", USER_AGENT);
1168
1749
  }
1169
- const target = proxied && typeof document !== "undefined" ? `${PROXY_ROUTE}?url=${encodeURIComponent(url)}` : url;
1750
+ const target = proxied && typeof document !== "undefined" ? `${DASHBOARD_PROXY_ROUTE}?url=${encodeURIComponent(url)}` : url;
1170
1751
  const res = await fetch(target, {
1171
1752
  ...init2,
1172
1753
  headers,
1173
1754
  signal: init2?.signal ?? AbortSignal.timeout(timeoutMs)
1174
1755
  });
1175
1756
  if (!res.ok) throw new Error(`${url} failed: ${res.status}`);
1757
+ return res;
1758
+ }
1759
+ async function fetchJson(url, schema, opts = {}) {
1760
+ const res = await request(url, opts);
1176
1761
  const body = await res.json();
1177
1762
  return schema ? schema.parse(body) : body;
1178
1763
  }
1179
1764
 
1180
1765
  // ../provider-alternativeme/src/index.ts
1181
1766
  var FNG_URL = "https://api.alternative.me/fng/";
1767
+ var sentimentCache = new TtlCache({
1768
+ namespace: "zframes:alternativeme:fng",
1769
+ ttlMs: 45 * 6e4,
1770
+ persist: true,
1771
+ revive: (value) => Array.isArray(value) ? value : null
1772
+ });
1182
1773
  var AlternativeMeProvider = class {
1183
1774
  name = "alternative.me";
1184
1775
  capabilities = ["sentiment"];
1185
1776
  async getFearGreed(limit = 30) {
1186
- const body = await fetchJson(`${FNG_URL}?limit=${limit}`);
1187
- if (!Array.isArray(body?.data))
1188
- throw new Error("alternative.me fng: unexpected response shape");
1189
- return body.data.map((entry) => ({
1190
- value: Number(entry.value),
1191
- classification: entry.value_classification ?? "",
1192
- time: Number(entry.timestamp) * 1e3
1193
- })).filter(
1194
- (point) => Number.isFinite(point.value) && Number.isFinite(point.time)
1195
- );
1777
+ return sentimentCache.get(String(limit), async () => {
1778
+ const body = await fetchJson(`${FNG_URL}?limit=${limit}`);
1779
+ if (!Array.isArray(body?.data))
1780
+ throw new Error("alternative.me fng: unexpected response shape");
1781
+ return body.data.map((entry) => ({
1782
+ value: Number(entry.value),
1783
+ classification: entry.value_classification ?? "",
1784
+ time: Number(entry.timestamp) * 1e3
1785
+ })).filter(
1786
+ (point) => Number.isFinite(point.value) && Number.isFinite(point.time)
1787
+ );
1788
+ });
1196
1789
  }
1197
1790
  };
1198
1791
 
1199
1792
  // ../provider-coingecko/src/index.ts
1200
1793
  var GLOBAL_URL = "https://api.coingecko.com/api/v3/global";
1201
- var CACHE_KEY = "zframes:coingecko:global";
1202
- var TTL_MS = 12 * 6e4;
1203
- var memo = null;
1204
- function readCache() {
1205
- if (memo) return memo;
1206
- try {
1207
- if (typeof localStorage !== "undefined") {
1208
- const raw = localStorage.getItem(CACHE_KEY);
1209
- if (raw) {
1210
- memo = JSON.parse(raw);
1211
- return memo;
1212
- }
1213
- }
1214
- } catch {
1215
- }
1216
- return null;
1217
- }
1218
- function writeCache(value) {
1219
- memo = { at: Date.now(), value };
1220
- try {
1221
- if (typeof localStorage !== "undefined")
1222
- localStorage.setItem(CACHE_KEY, JSON.stringify(memo));
1223
- } catch {
1224
- }
1225
- }
1794
+ var MARKETS_URL = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=50&page=1&sparkline=false&price_change_percentage=24h";
1795
+ var globalCache = new TtlCache({
1796
+ namespace: "zframes:coingecko:global",
1797
+ ttlMs: 12 * 6e4,
1798
+ persist: true
1799
+ });
1800
+ var marketsCache = new TtlCache({
1801
+ namespace: "zframes:coingecko:markets",
1802
+ ttlMs: 10 * 6e4,
1803
+ persist: true
1804
+ });
1226
1805
  var CoinGeckoProvider = class {
1227
1806
  name = "coingecko";
1228
- capabilities = ["global-market"];
1807
+ capabilities = [
1808
+ "global-market",
1809
+ "coin-markets"
1810
+ ];
1229
1811
  async getGlobalMarket() {
1230
- const cached = readCache();
1231
- if (cached && Date.now() - cached.at < TTL_MS) return cached.value;
1232
- try {
1812
+ return globalCache.get("global", async () => {
1233
1813
  const body = await fetchJson(GLOBAL_URL);
1234
1814
  if (!body?.data?.total_market_cap || !body.data.market_cap_percentage)
1235
1815
  throw new Error("coingecko global: unexpected response shape");
1236
1816
  const change = Number(body.data.market_cap_change_percentage_24h_usd);
1237
- const value = {
1817
+ return {
1238
1818
  totalMarketCapUsd: body.data.total_market_cap.usd ?? 0,
1239
1819
  marketCapChangePct24h: Number.isFinite(change) ? change : 0,
1240
1820
  dominance: body.data.market_cap_percentage
1241
1821
  };
1242
- writeCache(value);
1243
- return value;
1244
- } catch (err) {
1245
- if (cached) return cached.value;
1246
- throw err;
1247
- }
1822
+ });
1823
+ }
1824
+ async getCoinMarkets() {
1825
+ return marketsCache.get("markets", async () => {
1826
+ const body = await fetchJson(MARKETS_URL);
1827
+ if (!Array.isArray(body))
1828
+ throw new Error("coingecko markets: unexpected response shape");
1829
+ return body.filter((c) => Number.isFinite(c.market_cap) && c.market_cap > 0).map((c) => ({
1830
+ symbol: (c.symbol ?? "").toUpperCase(),
1831
+ name: c.name,
1832
+ marketCapUsd: c.market_cap,
1833
+ changePct24h: Number.isFinite(c.price_change_percentage_24h) ? c.price_change_percentage_24h : void 0
1834
+ }));
1835
+ });
1248
1836
  }
1249
1837
  };
1250
1838
 
1251
1839
  // ../provider-defillama/src/index.ts
1252
1840
  var CHAINS_URL = "https://api.llama.fi/v2/chains";
1841
+ var DEXS_URL = "https://api.llama.fi/overview/dexs";
1842
+ var FEES_URL = "https://api.llama.fi/overview/fees";
1843
+ var PROTOCOLS_URL = "https://api.llama.fi/protocols";
1844
+ var SNAPSHOT_TTL_MS = 8 * 6e4;
1845
+ var HISTORY_TTL_MS = 30 * 6e4;
1846
+ var tvlCache = new TtlCache({
1847
+ namespace: "zframes:defillama:tvl",
1848
+ ttlMs: SNAPSHOT_TTL_MS
1849
+ });
1850
+ var dexVolumeCache = new TtlCache({
1851
+ namespace: "zframes:defillama:dex-volume",
1852
+ ttlMs: SNAPSHOT_TTL_MS
1853
+ });
1854
+ var protocolTvlCache = new TtlCache({
1855
+ namespace: "zframes:defillama:protocol-tvl",
1856
+ ttlMs: SNAPSHOT_TTL_MS
1857
+ });
1858
+ var protocolFeesCache = new TtlCache({
1859
+ namespace: "zframes:defillama:protocol-fees",
1860
+ ttlMs: SNAPSHOT_TTL_MS
1861
+ });
1862
+ var dexHistoryCache = new TtlCache({
1863
+ namespace: "zframes:defillama:dex-history",
1864
+ ttlMs: HISTORY_TTL_MS
1865
+ });
1866
+ var protocolHistoryCache = new TtlCache({
1867
+ namespace: "zframes:defillama:protocol-history",
1868
+ ttlMs: HISTORY_TTL_MS
1869
+ });
1870
+ var slugKey = (slugs) => [...slugs].sort().join(",");
1871
+ function toSeries(chart) {
1872
+ if (!Array.isArray(chart)) return [];
1873
+ return chart.map(([ts, value]) => ({ time: ts * 1e3, value: Number(value) })).filter((p) => Number.isFinite(p.time) && Number.isFinite(p.value));
1874
+ }
1875
+ var changeOf = (v) => Number.isFinite(v) ? v : void 0;
1253
1876
  var DefiLlamaProvider = class {
1254
1877
  name = "defillama";
1255
- capabilities = ["tvl"];
1878
+ capabilities = [
1879
+ "tvl",
1880
+ "dex-volume",
1881
+ "protocol-tvl",
1882
+ "protocol-fees"
1883
+ ];
1256
1884
  async getTvlByChain() {
1257
- const chains = await fetchJson(CHAINS_URL);
1258
- if (!Array.isArray(chains))
1259
- throw new Error("defillama chains: unexpected response shape");
1260
- return chains.filter((chain) => Number.isFinite(chain.tvl) && chain.tvl > 0).sort((a, b) => b.tvl - a.tvl).map((chain) => ({ name: chain.name, tvl: chain.tvl }));
1885
+ return tvlCache.get("chains", async () => {
1886
+ const chains = await fetchJson(CHAINS_URL);
1887
+ if (!Array.isArray(chains))
1888
+ throw new Error("defillama chains: unexpected response shape");
1889
+ return chains.filter((chain) => Number.isFinite(chain.tvl) && chain.tvl > 0).sort((a, b) => b.tvl - a.tvl).map((chain) => ({ name: chain.name, tvl: chain.tvl }));
1890
+ });
1891
+ }
1892
+ async getDexVolume() {
1893
+ return dexVolumeCache.get("overview", async () => {
1894
+ const body = await fetchJson(DEXS_URL);
1895
+ const protocols = body?.protocols;
1896
+ if (!Array.isArray(protocols))
1897
+ throw new Error("defillama dexs: unexpected response shape");
1898
+ return protocols.filter((p) => Number.isFinite(p.total24h) && (p.total24h ?? 0) > 0).map((p) => ({
1899
+ name: p.name,
1900
+ volume24h: p.total24h,
1901
+ changePct: changeOf(p.change_1d)
1902
+ })).sort((a, b) => b.volume24h - a.volume24h);
1903
+ });
1904
+ }
1905
+ async getDexVolumeHistory(slugs) {
1906
+ return dexHistoryCache.get(slugKey(slugs), async () => {
1907
+ const pairs = await Promise.all(
1908
+ slugs.map(
1909
+ (slug) => fetchJson(
1910
+ `https://api.llama.fi/summary/dexs/${encodeURIComponent(
1911
+ slug
1912
+ )}?excludeTotalDataChartBreakdown=true`
1913
+ ).then((body) => [slug, toSeries(body.totalDataChart)]).catch(() => [slug, []])
1914
+ )
1915
+ );
1916
+ return Object.fromEntries(pairs);
1917
+ });
1918
+ }
1919
+ async getProtocolTvl() {
1920
+ return protocolTvlCache.get("overview", async () => {
1921
+ const protocols = await fetchJson(PROTOCOLS_URL);
1922
+ if (!Array.isArray(protocols))
1923
+ throw new Error("defillama protocols: unexpected response shape");
1924
+ return protocols.filter((p) => Number.isFinite(p.tvl) && (p.tvl ?? 0) > 0).map((p) => ({
1925
+ name: p.name,
1926
+ tvl: p.tvl,
1927
+ category: p.category,
1928
+ changePct: changeOf(p.change_1d)
1929
+ })).sort((a, b) => b.tvl - a.tvl);
1930
+ });
1931
+ }
1932
+ async getProtocolTvlHistory(slugs) {
1933
+ return protocolHistoryCache.get(slugKey(slugs), async () => {
1934
+ const pairs = await Promise.all(
1935
+ slugs.map(
1936
+ (slug) => fetchJson(
1937
+ `https://api.llama.fi/protocol/${encodeURIComponent(slug)}`
1938
+ ).then(
1939
+ (body) => [
1940
+ slug,
1941
+ (body.tvl ?? []).map((p) => ({
1942
+ time: p.date * 1e3,
1943
+ value: Number(p.totalLiquidityUSD)
1944
+ })).filter(
1945
+ (p) => Number.isFinite(p.time) && Number.isFinite(p.value)
1946
+ )
1947
+ ]
1948
+ ).catch(() => [slug, []])
1949
+ )
1950
+ );
1951
+ return Object.fromEntries(pairs);
1952
+ });
1953
+ }
1954
+ async getProtocolFees() {
1955
+ return protocolFeesCache.get("overview", async () => {
1956
+ const body = await fetchJson(FEES_URL);
1957
+ const protocols = body?.protocols;
1958
+ if (!Array.isArray(protocols))
1959
+ throw new Error("defillama fees: unexpected response shape");
1960
+ return protocols.filter((p) => Number.isFinite(p.total24h) && (p.total24h ?? 0) > 0).map((p) => ({
1961
+ name: p.name,
1962
+ fees24h: p.total24h,
1963
+ changePct: changeOf(p.change_1d)
1964
+ })).sort((a, b) => b.fees24h - a.fees24h);
1965
+ });
1261
1966
  }
1262
1967
  };
1263
1968
 
@@ -1280,7 +1985,8 @@ var HyperliquidProvider = class {
1280
1985
  "quote-stream",
1281
1986
  "day-stats",
1282
1987
  "funding-history",
1283
- "ohlcv"
1988
+ "ohlcv",
1989
+ "open-interest"
1284
1990
  ];
1285
1991
  ws = null;
1286
1992
  listeners = /* @__PURE__ */ new Set();
@@ -1339,6 +2045,39 @@ var HyperliquidProvider = class {
1339
2045
  );
1340
2046
  return out;
1341
2047
  }
2048
+ async getOpenInterest(symbols) {
2049
+ const wholeDexes = /* @__PURE__ */ new Set();
2050
+ const concrete = /* @__PURE__ */ new Set();
2051
+ if (!symbols) {
2052
+ wholeDexes.add("");
2053
+ } else {
2054
+ for (const s of symbols) {
2055
+ if (s.endsWith(":*")) wholeDexes.add(s.slice(0, -2));
2056
+ else concrete.add(s);
2057
+ }
2058
+ }
2059
+ const dexes = new Set(wholeDexes);
2060
+ for (const s of concrete) dexes.add(dexOf(s));
2061
+ const out = [];
2062
+ await Promise.all(
2063
+ [...dexes].map(async (dex) => {
2064
+ const body = { type: "metaAndAssetCtxs" };
2065
+ if (dex) body.dex = dex;
2066
+ const [meta, ctxs] = await info(body);
2067
+ const wholeDex = wholeDexes.has(dex);
2068
+ meta.universe.forEach((asset, i) => {
2069
+ if (!wholeDex && !concrete.has(asset.name)) return;
2070
+ const ctx = ctxs[i];
2071
+ if (!ctx) return;
2072
+ const markPx = Number(ctx.markPx);
2073
+ const oi = Number(ctx.openInterest);
2074
+ if (!Number.isFinite(markPx) || !Number.isFinite(oi)) return;
2075
+ out.push({ symbol: asset.name, openInterestUsd: oi * markPx });
2076
+ });
2077
+ })
2078
+ );
2079
+ return out.sort((a, b) => b.openInterestUsd - a.openInterestUsd);
2080
+ }
1342
2081
  async getFundingHistory(symbols, startTimeMs) {
1343
2082
  const results = await Promise.all(
1344
2083
  symbols.map(async (coin) => {
@@ -1573,7 +2312,7 @@ usage:
1573
2312
  frames) for the agent to fill in; --title <t>,
1574
2313
  --author <a>, --force to overwrite
1575
2314
  zframes serve [file] serve <dashboard.json> (default: ./dashboard.json)
1576
- as a live, editable terminal at 127.0.0.1:37263
2315
+ as a live, editable terminal at localhost:37263
1577
2316
  (--port <n> to change); Save writes back to the file
1578
2317
  zframes catalogue print the frame catalogue as JSON Schema
1579
2318
  (this is what a generating agent reads)
@@ -1584,56 +2323,6 @@ usage:
1584
2323
  stdout (the deterministic half of /zframes-brief)
1585
2324
  zframes help this text
1586
2325
  `;
1587
- function lintSpec(spec) {
1588
- const issues = [];
1589
- const metaByName = new Map(frameMetas.map((meta) => [meta.name, meta]));
1590
- const seenIds = /* @__PURE__ */ new Set();
1591
- for (const instance of spec.frames) {
1592
- if (seenIds.has(instance.id))
1593
- issues.push({
1594
- frameId: instance.id,
1595
- message: `duplicate frame id "${instance.id}"`
1596
- });
1597
- seenIds.add(instance.id);
1598
- const meta = metaByName.get(instance.frame);
1599
- if (!meta) {
1600
- issues.push({
1601
- frameId: instance.id,
1602
- message: `unknown frame "${instance.frame}". available: ${[
1603
- ...metaByName.keys()
1604
- ].join(", ")}`
1605
- });
1606
- continue;
1607
- }
1608
- const parsed = meta.schema.safeParse(instance.config);
1609
- if (!parsed.success) {
1610
- for (const issue of parsed.error.issues) {
1611
- issues.push({
1612
- frameId: instance.id,
1613
- message: `config.${issue.path.join(".") || "(root)"}: ${issue.message}`
1614
- });
1615
- }
1616
- }
1617
- if (instance.position.x + instance.position.w > spec.grid.columns)
1618
- issues.push({
1619
- frameId: instance.id,
1620
- message: `overflows the grid: x(${instance.position.x}) + w(${instance.position.w}) > ${spec.grid.columns} columns`
1621
- });
1622
- }
1623
- for (let i = 0; i < spec.frames.length; i++) {
1624
- for (let j = i + 1; j < spec.frames.length; j++) {
1625
- const a = spec.frames[i].position;
1626
- const b = spec.frames[j].position;
1627
- const overlap = a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
1628
- if (overlap)
1629
- issues.push({
1630
- frameId: spec.frames[i].id,
1631
- message: `overlaps frame "${spec.frames[j].id}"`
1632
- });
1633
- }
1634
- }
1635
- return issues;
1636
- }
1637
2326
  function lint(file) {
1638
2327
  let raw;
1639
2328
  try {
@@ -1698,7 +2387,16 @@ async function main() {
1698
2387
  return 1;
1699
2388
  }
1700
2389
  }
1701
- main().then((code) => process.exit(code));
1702
- export {
1703
- lintSpec
1704
- };
2390
+ main().then(
2391
+ (code) => {
2392
+ if (process.stdout.write("")) {
2393
+ process.exit(code);
2394
+ } else {
2395
+ process.stdout.once("drain", () => process.exit(code));
2396
+ }
2397
+ },
2398
+ (error) => {
2399
+ console.error(error);
2400
+ process.exit(1);
2401
+ }
2402
+ );