zframes 0.2.0 → 0.3.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.
Files changed (68) hide show
  1. package/dist/index.js +866 -54
  2. package/package.json +5 -5
  3. package/runtime/assets/editor-DVTcqJo4.js +68 -0
  4. package/runtime/assets/editor-DnV0I9s-.css +1 -0
  5. package/runtime/assets/index-DEGnog6Q.css +1 -0
  6. package/runtime/assets/index-KRy1feFG.js +761 -0
  7. package/runtime/index.html +3 -3
  8. package/runtime/widget-icons/breakeven.png +0 -0
  9. package/runtime/widget-icons/breathing.png +0 -0
  10. package/runtime/widget-icons/btc-blocks.png +0 -0
  11. package/runtime/widget-icons/btc-difficulty.png +0 -0
  12. package/runtime/widget-icons/btc-fees.png +0 -0
  13. package/runtime/widget-icons/btc-hashrate.png +0 -0
  14. package/runtime/widget-icons/btc-mempool.png +0 -0
  15. package/runtime/widget-icons/calculator.png +0 -0
  16. package/runtime/widget-icons/checklist.png +0 -0
  17. package/runtime/widget-icons/coin-movers.png +0 -0
  18. package/runtime/widget-icons/countdown.png +0 -0
  19. package/runtime/widget-icons/day-meter.png +0 -0
  20. package/runtime/widget-icons/dex-volume-chart.png +0 -0
  21. package/runtime/widget-icons/dex-volume-treemap.png +0 -0
  22. package/runtime/widget-icons/dice.png +0 -0
  23. package/runtime/widget-icons/divider.png +0 -0
  24. package/runtime/widget-icons/drawdy.png +0 -0
  25. package/runtime/widget-icons/filings-feed.png +0 -0
  26. package/runtime/widget-icons/financial-stress.png +0 -0
  27. package/runtime/widget-icons/flappy-bird.png +0 -0
  28. package/runtime/widget-icons/fundamentals.png +0 -0
  29. package/runtime/widget-icons/holiday-calendar.png +0 -0
  30. package/runtime/widget-icons/journal-log.png +0 -0
  31. package/runtime/widget-icons/journal-open.png +0 -0
  32. package/runtime/widget-icons/journal-results.png +0 -0
  33. package/runtime/widget-icons/journal-score.png +0 -0
  34. package/runtime/widget-icons/labor-market.png +0 -0
  35. package/runtime/widget-icons/lightning-stats.png +0 -0
  36. package/runtime/widget-icons/link-grid.png +0 -0
  37. package/runtime/widget-icons/market-cap-treemap.png +0 -0
  38. package/runtime/widget-icons/marquee.png +0 -0
  39. package/runtime/widget-icons/mining-pools.png +0 -0
  40. package/runtime/widget-icons/national-debt.png +0 -0
  41. package/runtime/widget-icons/news-feed.png +0 -0
  42. package/runtime/widget-icons/open-interest.png +0 -0
  43. package/runtime/widget-icons/options-iv.png +0 -0
  44. package/runtime/widget-icons/options-oi-strike.png +0 -0
  45. package/runtime/widget-icons/options-put-call.png +0 -0
  46. package/runtime/widget-icons/pomodoro.png +0 -0
  47. package/runtime/widget-icons/portfolio-allocation.png +0 -0
  48. package/runtime/widget-icons/portfolio-holdings.png +0 -0
  49. package/runtime/widget-icons/portfolio-value.png +0 -0
  50. package/runtime/widget-icons/protocol-fees-treemap.png +0 -0
  51. package/runtime/widget-icons/protocol-tvl-chart.png +0 -0
  52. package/runtime/widget-icons/protocol-tvl-treemap.png +0 -0
  53. package/runtime/widget-icons/quote.png +0 -0
  54. package/runtime/widget-icons/returns-projector.png +0 -0
  55. package/runtime/widget-icons/risk-reward.png +0 -0
  56. package/runtime/widget-icons/rules-card.png +0 -0
  57. package/runtime/widget-icons/session-progress.png +0 -0
  58. package/runtime/widget-icons/short-volume.png +0 -0
  59. package/runtime/widget-icons/snake.png +0 -0
  60. package/runtime/widget-icons/spotify-embed.png +0 -0
  61. package/runtime/widget-icons/stopwatch.png +0 -0
  62. package/runtime/widget-icons/treasury-auctions.png +0 -0
  63. package/runtime/widget-icons/video.png +0 -0
  64. package/runtime/widget-icons/yield-curve.png +0 -0
  65. package/runtime/assets/editor-CY2XpG9x.js +0 -68
  66. package/runtime/assets/editor-DinyMxo6.css +0 -1
  67. package/runtime/assets/index-Bpmi3x1d.js +0 -597
  68. package/runtime/assets/index-CsheOEUr.css +0 -1
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ function catalogueForAI(input) {
9
9
  const metas = input instanceof Map ? [...input.values()] : [...input];
10
10
  return metas.map((meta) => ({
11
11
  name: meta.name,
12
+ category: meta.category,
12
13
  description: meta.description,
13
14
  iconUrl: meta.iconUrl,
14
15
  capabilities: meta.capabilities,
@@ -17,6 +18,30 @@ function catalogueForAI(input) {
17
18
  configSchema: z.toJSONSchema(meta.schema, { io: "input" })
18
19
  }));
19
20
  }
21
+ function catalogueSummary(input) {
22
+ const metas = input instanceof Map ? [...input.values()] : [...input];
23
+ const order = [];
24
+ const byCategory = /* @__PURE__ */ new Map();
25
+ for (const meta of metas) {
26
+ let group = byCategory.get(meta.category);
27
+ if (!group) {
28
+ group = [];
29
+ byCategory.set(meta.category, group);
30
+ order.push(meta.category);
31
+ }
32
+ group.push(meta);
33
+ }
34
+ const lines = [];
35
+ for (const category of order) {
36
+ const items = (byCategory.get(category) ?? []).map((meta) => {
37
+ const desc = meta.description.replace(/\s+/g, " ").trim();
38
+ const short = desc.length > 110 ? `${desc.slice(0, 109)}\u2026` : desc;
39
+ return `${meta.name} \u2014 ${short}`;
40
+ }).join("; ");
41
+ lines.push(`${category}: ${items}`);
42
+ }
43
+ return lines.join("\n");
44
+ }
20
45
 
21
46
  // ../core/src/spec.ts
22
47
  import { z as z2 } from "zod";
@@ -33,6 +58,9 @@ var FrameInstanceSchema = z2.object({
33
58
  `Card title shown in the frame's chrome. Overrides the default (the frame type name) \u2014 set a meaningful per-instance label, e.g. the ticker on a price-chart ("TSLA"). Ignored by chrome-less frames like heading.`
34
59
  ),
35
60
  position: GridPositionSchema,
61
+ layouts: z2.record(z2.string(), GridPositionSchema).optional().describe(
62
+ 'Per-mode layout overrides keyed by grid.mode (e.g. "flow-horizontal"). `position` is the canonical flow-vertical layout; this holds the other modes\' placements so each layout mode keeps its own independent arrangement. Editor-managed \u2014 agents only need to set `position`.'
63
+ ),
36
64
  config: z2.record(z2.string(), z2.unknown()).default({}).describe(
37
65
  "Frame config; validated against the frame's own schema at render time"
38
66
  )
@@ -56,8 +84,35 @@ var ThemeSchema = z2.object({
56
84
  ),
57
85
  accentSat: z2.number().min(0).max(100).default(90).describe(
58
86
  "Accent saturation as an HSL percentage (0\u2013100). 90 is the default vivid zframes accent; lower it for muted/pastel rims and dots, 0 for a near-grayscale monochrome accent. Pairs with accentHue \u2014 hue picks the color, saturation picks how vivid it is."
87
+ ),
88
+ baseHue: z2.number().int().min(0).max(360).default(233).describe(
89
+ "Hue of the dark card surface itself, in degrees (0\u2013360). 233 is the default near-black indigo-navy; rotate it to re-temperature every card \u2014 e.g. 30 warms toward charcoal-brown, 150 toward deep forest, 210 toward cool slate. The surface stays dark (lightness is fixed); only its tint moves. Independent of accentHue, which only colours rims/highlights."
90
+ ),
91
+ baseSat: z2.number().min(0).max(100).default(20).describe(
92
+ "Saturation of the dark card surface as an HSL percentage (0\u2013100). 20 is the default subtle navy tint; raise it for a richer coloured-black surface, drop it toward 0 for a neutral graphite/black with no colour cast. Pairs with baseHue."
93
+ ),
94
+ upColor: z2.string().regex(/^#[0-9a-fA-F]{6}$/, "must be a 6-digit hex colour like #3fd08f").default("#3fd08f").describe(
95
+ "Semantic colour for gains / positive change / bullish (up). 6-digit hex; default #3fd08f (green). Tints every gain delta, mover row, up-candle, and 'long'/'call' marker. Set this and downColor to a non-green/red pair (e.g. blue/orange) for a colourblind-friendly dashboard. Distinct from the accent (which is brand colour, not meaning) and from the error red."
96
+ ),
97
+ downColor: z2.string().regex(/^#[0-9a-fA-F]{6}$/, "must be a 6-digit hex colour like #ff6b81").default("#ff6b81").describe(
98
+ "Semantic colour for losses / negative change / bearish (down). 6-digit hex; default #ff6b81 (red). The counterpart to upColor \u2014 tints every loss delta, mover row, down-candle, and 'short'/'put' marker."
99
+ )
100
+ }).describe(
101
+ "Dashboard-wide colour identity \u2014 accent (rims/highlights), the dark card-surface tint (base), and the semantic up/down (gain/loss) colours."
102
+ );
103
+ var TypographySchema = z2.object({
104
+ fontFamily: z2.enum(["sans", "mono", "serif"]).default("sans").describe(
105
+ "Type family for all dashboard text. 'sans' is the default DM Sans (clean, modern); 'mono' switches to a monospaced system stack (a Bloomberg/terminal feel, numbers in fixed columns); 'serif' uses a system serif (editorial). Affects card titles, labels, and readouts alike."
106
+ ),
107
+ numericStyle: z2.enum(["proportional", "tabular"]).default("proportional").describe(
108
+ "How digits are spaced everywhere on the dashboard. 'proportional' is the default (natural widths); 'tabular' forces fixed-width figures (font-variant-numeric: tabular-nums) so live prices and tickers stop shifting sideways as digits change \u2014 recommended for number-dense dashboards. Hero stat numerals are always tabular regardless."
109
+ ),
110
+ scale: z2.number().min(0.85).max(1.25).default(1).describe(
111
+ "Global text-size multiplier (0.85\u20131.25). 1 is the default. Scales all dashboard text together by setting the root font size, so chart labels, readouts, and titles grow or shrink as one \u2014 raise it for legibility on a large display, lower it to pack more in. Distinct from appearance.density, which scales card padding, not text. Grid cell heights are in pixels and stay fixed, so text can overflow a small card at high scale."
59
112
  )
60
- }).describe("Dashboard-wide color identity (accent hue + saturation).");
113
+ }).describe(
114
+ "Dashboard-wide typography \u2014 font family, numeric digit style, and global text scale."
115
+ );
61
116
  var AppearanceSchema = z2.object({
62
117
  radius: z2.number().min(0).default(18).describe(
63
118
  "Frame corner radius in pixels, applied to every card via --zf-frame-radius. 0 squares the corners; the editor's Appearance rail exposes this as a slider."
@@ -104,19 +159,47 @@ var DashboardSpecSchema = z2.preprocess(
104
159
  `Who made this dashboard \u2014 a free-form credit, like package.json's author ("You" or "You <you@example.com>"). Optional.`
105
160
  ),
106
161
  grid: z2.object({
107
- columns: z2.number().int().min(1).default(12),
108
- rowHeight: z2.number().min(8).default(96),
162
+ mode: z2.enum(["flow-vertical", "flow-horizontal", "canvas"]).default("flow-vertical").describe(
163
+ "Layout model. 'flow-vertical' (default): a fixed number of columns fill the viewport width; the board grows downward and scrolls vertically \u2014 the classic dashboard. 'flow-horizontal': a fixed number of rows fill the viewport height; the board grows rightward and scrolls horizontally \u2014 suited to ultrawide and wall displays. 'canvas': an unbounded pan/zoom plane (not yet implemented; rendered as flow-vertical for now)."
164
+ ),
165
+ columns: z2.number().int().min(1).default(12).describe(
166
+ "Number of columns the grid is divided into across the viewport width (flow-vertical). A frame's `position` x/w are in these column units."
167
+ ),
168
+ rowHeight: z2.number().min(8).default(96).describe(
169
+ "Pixel height of each grid row (flow-vertical). A frame's `position` y/h are in these row units."
170
+ ),
171
+ rows: z2.number().int().min(1).default(6).describe(
172
+ 'Number of height-bounded rows the horizontal board (flow-horizontal) fills before it scrolls sideways. A frame\'s `layouts["flow-horizontal"]` y/h are in these row units. Ignored in flow-vertical.'
173
+ ),
109
174
  gap: z2.number().min(0).default(12).describe(
110
175
  "Pixels of space between frames (the grid gutter). 0 makes the cards flush; the editor's Layout rail exposes this as a slider."
111
176
  )
112
- }).default({ columns: 12, rowHeight: 96, gap: 12 }),
177
+ }).default({
178
+ mode: "flow-vertical",
179
+ columns: 12,
180
+ rowHeight: 96,
181
+ rows: 6,
182
+ gap: 12
183
+ }),
113
184
  background: BackgroundSchema.default({
114
185
  type: "gradient",
115
186
  scale: 1,
116
187
  dpi: 1.5,
117
188
  opacity: 0.16
118
189
  }),
119
- theme: ThemeSchema.default({ accentHue: 242, accentSat: 90 }),
190
+ theme: ThemeSchema.default({
191
+ accentHue: 242,
192
+ accentSat: 90,
193
+ baseHue: 233,
194
+ baseSat: 20,
195
+ upColor: "#3fd08f",
196
+ downColor: "#ff6b81"
197
+ }),
198
+ typography: TypographySchema.default({
199
+ fontFamily: "sans",
200
+ numericStyle: "proportional",
201
+ scale: 1
202
+ }),
120
203
  appearance: AppearanceSchema.default({
121
204
  radius: 18,
122
205
  borderStrength: 0.22,
@@ -165,6 +248,7 @@ var SOURCES = {
165
248
  };
166
249
  var clockMeta = defineFrameMeta({
167
250
  name: "clock",
251
+ category: "tools",
168
252
  iconUrl: widgetIcon("clock"),
169
253
  layout: { w: 3, h: 2, minW: 2, minH: 1 },
170
254
  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.",
@@ -189,6 +273,7 @@ var clockMeta = defineFrameMeta({
189
273
  });
190
274
  var marketHoursMeta = defineFrameMeta({
191
275
  name: "market-hours",
276
+ category: "tools",
192
277
  iconUrl: widgetIcon("market-hours"),
193
278
  layout: { w: 4, h: 4, minW: 3, minH: 3 },
194
279
  description: "Which world stock exchanges are open right now \u2014 each row shows an open / closed / holiday status dot and a live countdown to the next open or close. Computed entirely client-side from each exchange's timezone and regular trading hours (no API); a bundled 2026 holiday list keeps the major Western exchanges accurate on market holidays. Intraday lunch breaks and half-day early closes are not modelled. Needs no data provider.",
@@ -204,6 +289,7 @@ var marketHoursMeta = defineFrameMeta({
204
289
  });
205
290
  var fearGreedMeta = defineFrameMeta({
206
291
  name: "fear-greed",
292
+ category: "sentiment",
207
293
  iconUrl: widgetIcon("fear-greed"),
208
294
  layout: { w: 3, h: 3, minW: 2, minH: 2 },
209
295
  description: "Crypto Fear & Greed index (0 = extreme fear, 100 = extreme greed) with a recent-history sparkline. A one-number market mood gauge from alternative.me.",
@@ -215,6 +301,7 @@ var fearGreedMeta = defineFrameMeta({
215
301
  });
216
302
  var fundingRateChartMeta = defineFrameMeta({
217
303
  name: "funding-rate-chart",
304
+ category: "derivatives",
218
305
  iconUrl: widgetIcon("funding-rate-chart"),
219
306
  layout: { w: 6, h: 3, minW: 3, minH: 2 },
220
307
  description: "Multi-series line chart comparing hourly perp funding rates across symbols over a configurable lookback window. Positive funding = longs pay shorts. Useful for spotting crowded trades.",
@@ -229,6 +316,7 @@ var fundingRateChartMeta = defineFrameMeta({
229
316
  });
230
317
  var noteMeta = defineFrameMeta({
231
318
  name: "note",
319
+ category: "layout",
232
320
  iconUrl: widgetIcon("note"),
233
321
  layout: { w: 4, h: 3, minW: 2, minH: 2 },
234
322
  description: "Free-form text note pinned to the dashboard \u2014 trading plans, reminders, watch levels. Needs no data provider.",
@@ -240,6 +328,7 @@ var noteMeta = defineFrameMeta({
240
328
  });
241
329
  var priceChartMeta = defineFrameMeta({
242
330
  name: "price-chart",
331
+ category: "markets",
243
332
  iconUrl: widgetIcon("price-chart"),
244
333
  layout: { w: 6, h: 3, minW: 3, minH: 2 },
245
334
  description: "Live animated price chart (candlestick or line) for one symbol \u2014 canvas-rendered at 60fps via liveline, streaming live off the Hyperliquid WebSocket. Works for HIP-3 stock perps (xyz:TSLA) and crypto (BTC). The centerpiece frame.",
@@ -256,6 +345,7 @@ var priceChartMeta = defineFrameMeta({
256
345
  });
257
346
  var priceLivelineMeta = defineFrameMeta({
258
347
  name: "price-liveline",
348
+ category: "markets",
259
349
  iconUrl: widgetIcon("price-liveline"),
260
350
  layout: { w: 6, h: 3, minW: 4, minH: 2 },
261
351
  description: "Multi-asset live price liveline \u2014 several Hyperliquid symbols streaming in one canvas chart. Defaults to normalized % movement so stocks and crypto can share one axis, while the legend still shows each asset's live raw price. Use when the dashboard needs one compact live race view instead of several single-symbol charts.",
@@ -275,6 +365,7 @@ var priceLivelineMeta = defineFrameMeta({
275
365
  });
276
366
  var priceTickerMeta = defineFrameMeta({
277
367
  name: "price-ticker",
368
+ category: "markets",
278
369
  iconUrl: widgetIcon("price-ticker"),
279
370
  layout: { w: 3, h: 3, minW: 2, minH: 2 },
280
371
  description: "Live watchlist streaming mid prices over the Hyperliquid WebSocket with 24h change per symbol. The bread-and-butter frame for any dashboard.",
@@ -288,6 +379,7 @@ var priceTickerMeta = defineFrameMeta({
288
379
  });
289
380
  var topMoversMeta = defineFrameMeta({
290
381
  name: "top-movers",
382
+ category: "markets",
291
383
  iconUrl: widgetIcon("top-movers"),
292
384
  layout: { w: 5, h: 3, minW: 3, minH: 3 },
293
385
  description: "Today's biggest stock and commodity HIP-3 gainers and losers (no bare crypto), side by side with current price and 24h change.",
@@ -299,6 +391,7 @@ var topMoversMeta = defineFrameMeta({
299
391
  });
300
392
  var tvlTreemapMeta = defineFrameMeta({
301
393
  name: "tvl-treemap",
394
+ category: "crypto",
302
395
  iconUrl: widgetIcon("tvl-treemap"),
303
396
  layout: { w: 6, h: 4, minW: 3, minH: 3 },
304
397
  description: "Treemap of total value locked (TVL) across the largest blockchain ecosystems, sized by TVL. Data from DeFiLlama. Good single-glance answer to 'where does on-chain capital live right now'.",
@@ -310,6 +403,7 @@ var tvlTreemapMeta = defineFrameMeta({
310
403
  });
311
404
  var bitcoinDominanceMeta = defineFrameMeta({
312
405
  name: "bitcoin-dominance",
406
+ category: "crypto",
313
407
  iconUrl: widgetIcon("bitcoin-dominance"),
314
408
  layout: { w: 4, h: 2, minW: 3, minH: 2 },
315
409
  description: "BTC / ETH / Others market-cap dominance as a segmented bar, with optional total marketcap line. Shifts in BTC dominance hint at where the market rotates next.",
@@ -323,6 +417,7 @@ var bitcoinDominanceMeta = defineFrameMeta({
323
417
  });
324
418
  var ratesBoardMeta = defineFrameMeta({
325
419
  name: "rates-board",
420
+ category: "macro",
326
421
  iconUrl: widgetIcon("rates-board"),
327
422
  layout: { w: 4, h: 4, minW: 3, minH: 3 },
328
423
  description: "Official US rates board from free public APIs: New York Fed reference rates (SOFR, effective fed funds, repo rates) plus Treasury average interest rates by security class. Daily/reference data, not a real-time stock quote feed.",
@@ -338,6 +433,7 @@ var ratesBoardMeta = defineFrameMeta({
338
433
  });
339
434
  var inflationPulseMeta = defineFrameMeta({
340
435
  name: "inflation-pulse",
436
+ category: "macro",
341
437
  iconUrl: widgetIcon("inflation-pulse"),
342
438
  layout: { w: 4, h: 3, minW: 3, minH: 2 },
343
439
  description: "BLS CPI pulse from the public no-key API: latest CPI-U all-items index with month-over-month and year-over-year changes plus a small trend sparkline. Monthly macro context for stock dashboards; not a live price feed.",
@@ -349,6 +445,7 @@ var inflationPulseMeta = defineFrameMeta({
349
445
  });
350
446
  var financialStressMeta = defineFrameMeta({
351
447
  name: "financial-stress",
448
+ category: "macro",
352
449
  iconUrl: widgetIcon("financial-stress"),
353
450
  layout: { w: 4, h: 3, minW: 3, minH: 2 },
354
451
  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.",
@@ -363,6 +460,7 @@ var financialStressMeta = defineFrameMeta({
363
460
  });
364
461
  var nationalDebtMeta = defineFrameMeta({
365
462
  name: "national-debt",
463
+ category: "macro",
366
464
  iconUrl: widgetIcon("national-debt"),
367
465
  layout: { w: 4, h: 3, minW: 3, minH: 2 },
368
466
  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.",
@@ -379,6 +477,7 @@ var nationalDebtMeta = defineFrameMeta({
379
477
  });
380
478
  var laborMarketMeta = defineFrameMeta({
381
479
  name: "labor-market",
480
+ category: "macro",
382
481
  iconUrl: widgetIcon("labor-market"),
383
482
  layout: { w: 4, h: 3, minW: 3, minH: 2 },
384
483
  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.",
@@ -390,6 +489,7 @@ var laborMarketMeta = defineFrameMeta({
390
489
  });
391
490
  var treasuryAuctionsMeta = defineFrameMeta({
392
491
  name: "treasury-auctions",
492
+ category: "macro",
393
493
  iconUrl: widgetIcon("treasury-auctions"),
394
494
  layout: { w: 5, h: 4, minW: 3, minH: 3 },
395
495
  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.",
@@ -401,6 +501,7 @@ var treasuryAuctionsMeta = defineFrameMeta({
401
501
  });
402
502
  var filingsFeedMeta = defineFrameMeta({
403
503
  name: "filings-feed",
504
+ category: "equities",
404
505
  iconUrl: widgetIcon("filings-feed"),
405
506
  layout: { w: 5, h: 4, minW: 3, minH: 3 },
406
507
  description: "Recent SEC EDGAR filings for one US-listed company \u2014 each row shows the form type (10-K, 10-Q, 8-K, Form 4\u2026), a plain-English label, the filing date, and a click-through to the document on sec.gov, under a header with the company name, exchange, and filer category. Official data from SEC's free, CORS-safe submissions endpoint; event-driven (updates when the company files), not a price feed. Resolve by ticker (a bundled snapshot of the ~500 largest US issuers) or by raw SEC CIK for anything else.",
@@ -418,6 +519,7 @@ var filingsFeedMeta = defineFrameMeta({
418
519
  });
419
520
  var yieldCurveMeta = defineFrameMeta({
420
521
  name: "yield-curve",
522
+ category: "macro",
421
523
  iconUrl: widgetIcon("yield-curve"),
422
524
  layout: { w: 4, h: 3, minW: 3, minH: 3 },
423
525
  description: "The U.S. Treasury daily par yield curve \u2014 a line from 1-month to 30-year yields, the headline 2s10s spread (10Y minus 2Y; negative = inverted, the classic recession signal), and a configurable row of key maturities. Keyless official data from the U.S. Treasury, updated each business day; not a live intraday feed.",
@@ -447,6 +549,7 @@ var yieldCurveMeta = defineFrameMeta({
447
549
  });
448
550
  var fundamentalsMeta = defineFrameMeta({
449
551
  name: "fundamentals",
552
+ category: "equities",
450
553
  iconUrl: widgetIcon("fundamentals"),
451
554
  layout: { w: 4, h: 3, minW: 3, minH: 3 },
452
555
  description: "Headline financials for one US-listed company from SEC EDGAR XBRL company facts \u2014 revenue, net income, total assets, shareholders' equity, diluted EPS, and shares outstanding, each labelled with its fiscal period. Income-statement figures are the latest full fiscal year; balance-sheet figures are the latest reported quarter. Keyless official data that updates only when the company files (annual/quarterly), not a live feed. Requires the zframes runtime's data proxy (it ships with `zframes serve` / `vite dev`); resolve by ticker (bundled top-500 map) or raw SEC CIK.",
@@ -460,6 +563,7 @@ var fundamentalsMeta = defineFrameMeta({
460
563
  });
461
564
  var shortVolumeMeta = defineFrameMeta({
462
565
  name: "short-volume",
566
+ category: "equities",
463
567
  iconUrl: widgetIcon("short-volume"),
464
568
  layout: { w: 5, h: 4, minW: 3, minH: 3 },
465
569
  description: "Daily reported short-sale volume for a watchlist of US-listed stocks, from FINRA's free consolidated file \u2014 each row shows the % of the day's reported volume that was sold short, with a bar and the raw short/total share counts. IMPORTANT: this is reported short volume (sell-side short flow, which includes market-maker hedging), NOT short interest (outstanding short positions), and is not a directional signal on its own. Daily data published the next business day; not a live feed. US equities only.",
@@ -476,6 +580,7 @@ var shortVolumeMeta = defineFrameMeta({
476
580
  });
477
581
  var fundingHeatmapMeta = defineFrameMeta({
478
582
  name: "funding-heatmap",
583
+ category: "derivatives",
479
584
  iconUrl: widgetIcon("funding-heatmap"),
480
585
  layout: { w: 6, h: 3, minW: 4, minH: 3 },
481
586
  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.",
@@ -489,6 +594,7 @@ var fundingHeatmapMeta = defineFrameMeta({
489
594
  });
490
595
  var dinoGameMeta = defineFrameMeta({
491
596
  name: "dino-game",
597
+ category: "games",
492
598
  iconUrl: widgetIcon("dino-game"),
493
599
  layout: { w: 4, h: 3, minW: 3, minH: 3 },
494
600
  description: "Chrome-dino style runner game on canvas \u2014 jump cacti with SPACE or tap. High score persists locally. For when the market is boring. Needs no data provider.",
@@ -497,6 +603,7 @@ var dinoGameMeta = defineFrameMeta({
497
603
  });
498
604
  var imageMeta = defineFrameMeta({
499
605
  name: "image",
606
+ category: "layout",
500
607
  iconUrl: widgetIcon("image"),
501
608
  layout: { w: 3, h: 3, minW: 1, minH: 1 },
502
609
  description: "Displays an image from a URL \u2014 logos, memes, chart screenshots, banners. Needs no data provider.",
@@ -511,6 +618,7 @@ var imageMeta = defineFrameMeta({
511
618
  });
512
619
  var headingMeta = defineFrameMeta({
513
620
  name: "heading",
621
+ category: "layout",
514
622
  iconUrl: widgetIcon("heading"),
515
623
  layout: { w: 12, h: 1, minW: 2, minH: 1, maxH: 1 },
516
624
  description: "Section divider that titles a region of the dashboard ('Markets', 'On-chain', 'Desk'). Renders as a label with a hairline rule \u2014 no card. Use to group frames into zones: place full-width (w: 12) and 1 row tall (h: 1) above each group. Needs no data provider.",
@@ -523,6 +631,7 @@ var headingMeta = defineFrameMeta({
523
631
  });
524
632
  var dailyAnalysisMeta = defineFrameMeta({
525
633
  name: "daily-analysis",
634
+ category: "tools",
526
635
  iconUrl: widgetIcon("daily-analysis"),
527
636
  layout: { w: 6, h: 3, minW: 3, minH: 2 },
528
637
  description: "Daily market brief written by the /zframes-brief loop \u2014 a dated analysis of the symbols on your dashboard, the calls it is making today, and how yesterday's calls scored (with a running hit-rate). Reads a local log file the loop appends to; needs no market data provider. Add one per dashboard.",
@@ -539,8 +648,51 @@ var dailyAnalysisMeta = defineFrameMeta({
539
648
  )
540
649
  })
541
650
  });
651
+ var journalLogMeta = defineFrameMeta({
652
+ name: "journal-log",
653
+ category: "journal",
654
+ iconUrl: widgetIcon("journal-log"),
655
+ layout: { w: 4, h: 5, minW: 3, minH: 4 },
656
+ source: SOURCES.hyperliquid,
657
+ description: "Log a market read in seconds: pick a supported ticker (with its live Hyperliquid price), Long or Short, the reason (a quick pick + optional note), and how sure you are (a slider). That's it \u2014 a falsifiable call, captured at the live price, that the Open/Results frames then track and grade. The simple front door to your decision journal; pairs with the zAI orb for conversational capture. Add one alongside Journal \xB7 Open and Journal \xB7 Results.",
658
+ capabilities: ["quote-stream", "day-stats"],
659
+ schema: z3.object({})
660
+ });
661
+ var journalOpenMeta = defineFrameMeta({
662
+ name: "journal-open",
663
+ category: "journal",
664
+ iconUrl: widgetIcon("journal-open"),
665
+ layout: { w: 4, h: 4, minW: 3, minH: 3 },
666
+ description: "Your open calls from the decision journal, each marking to the live Hyperliquid price \u2014 direction, confidence, unrealized % return, a live entry\u2192target track, and a countdown. Calls auto-grade at their horizon (or close one early). The 'watch it play out' frame. Reads the journal you write with Journal \xB7 Log.",
667
+ source: SOURCES.hyperliquid,
668
+ capabilities: ["quote-stream"],
669
+ schema: z3.object({
670
+ max: z3.number().int().min(1).max(20).default(8).describe("How many open calls to show (newest first).")
671
+ })
672
+ });
673
+ var journalResultsMeta = defineFrameMeta({
674
+ name: "journal-results",
675
+ category: "journal",
676
+ iconUrl: widgetIcon("journal-results"),
677
+ layout: { w: 4, h: 4, minW: 3, minH: 3 },
678
+ description: "Your resolved calls from the decision journal, graded on TWO axes: did it hit, AND did the thesis actually play out \u2014 so a lucky hit reads differently from earned skill, and a near-miss from a clean miss. The reflection frame. Reads the journal you write with Journal \xB7 Log.",
679
+ capabilities: [],
680
+ schema: z3.object({
681
+ max: z3.number().int().min(1).max(20).default(8).describe("How many resolved calls to show (newest first).")
682
+ })
683
+ });
684
+ var journalScoreMeta = defineFrameMeta({
685
+ name: "journal-score",
686
+ category: "journal",
687
+ iconUrl: widgetIcon("journal-score"),
688
+ layout: { w: 4, h: 2, minW: 3, minH: 2 },
689
+ description: "The decision-journal scoreboard \u2014 a story, not a spreadsheet: where your judgment has an edge, where it leaks, and how calibrated your confidence is, plus a one-line read from zAI. Aggregates the calls logged via Journal \xB7 Log.",
690
+ capabilities: [],
691
+ schema: z3.object({})
692
+ });
542
693
  var priceCompareMeta = defineFrameMeta({
543
694
  name: "price-compare",
695
+ category: "markets",
544
696
  iconUrl: widgetIcon("price-compare"),
545
697
  layout: { w: 6, h: 3, minW: 3, minH: 2 },
546
698
  description: "Multi-series line chart overlaying the price history of several symbols over a lookback window \u2014 see how TSLA, NVDA and BTC moved against each other. Normalized by default to % change from the window start so symbols at very different price levels (BTC vs a $20 stock) stay comparable on one axis. Candles from Hyperliquid.",
@@ -556,26 +708,53 @@ var priceCompareMeta = defineFrameMeta({
556
708
  )
557
709
  })
558
710
  });
559
- var allocationMeta = defineFrameMeta({
560
- name: "allocation",
561
- iconUrl: widgetIcon("allocation"),
562
- layout: { w: 4, h: 4, minW: 3, minH: 3 },
563
- description: "Donut of a portfolio's live allocation \u2014 list holdings (symbol + amount) and each slice is sized by current USD value off the Hyperliquid mid stream, with total portfolio value in the center. A live 'where is my money right now' view.",
564
- capabilities: ["quote-stream"],
565
- source: SOURCES.hyperliquid,
711
+ var portfolioConfigShape = {
712
+ source: z3.enum(["binance", "wallet"]).default("binance").describe(
713
+ 'Where the holdings come from: "binance" (a connected Binance account \u2014 a read-only API key is entered in-app and stored locally, never in this file) or "wallet" (a public on-chain address, keyless).'
714
+ ),
715
+ address: z3.string().default("").describe(
716
+ 'For source "wallet": the public Ethereum address (0x\u2026) or ENS name to track. Public on-chain data, no keys. Ignored for "binance".'
717
+ )
718
+ };
719
+ var portfolioValueMeta = defineFrameMeta({
720
+ name: "portfolio-value",
721
+ category: "portfolio",
722
+ iconUrl: widgetIcon("portfolio-value"),
723
+ layout: { w: 5, h: 4, minW: 3, minH: 3 },
724
+ description: "Your connected portfolio's total USD value as a live equity line, ticking with the market. Source is a connected Binance account (read-only key, entered in-app) or a public on-chain wallet address. Shows total value + session change. Renders a connect prompt until a source is set.",
725
+ capabilities: ["portfolio", "quote-stream"],
726
+ account: true,
566
727
  schema: z3.object({
567
- holdings: z3.array(
568
- z3.object({
569
- symbol: z3.string().min(1).describe('Hyperliquid symbol, e.g. "BTC", "ETH", "xyz:TSLA".'),
570
- amount: z3.number().positive().describe(
571
- "Units held (e.g. 0.5 BTC, 10 shares). Weights the slice by USD value = amount \xD7 live price."
572
- )
573
- })
574
- ).min(2).max(8).describe("The holdings to chart. 2 to 8 positions.")
728
+ ...portfolioConfigShape,
729
+ windowSec: z3.number().int().positive().default(300).describe(
730
+ "Seconds of live history the equity line shows; it accumulates from when the dashboard opens."
731
+ )
575
732
  })
576
733
  });
734
+ var portfolioAllocationMeta = defineFrameMeta({
735
+ name: "portfolio-allocation",
736
+ category: "portfolio",
737
+ iconUrl: widgetIcon("portfolio-allocation"),
738
+ layout: { w: 4, h: 4, minW: 3, minH: 3 },
739
+ description: "Donut of your connected portfolio's allocation \u2014 each slice sized by live USD value, total in the center. Source is a connected Binance account (read-only key, in-app) or a public on-chain wallet address. Renders a connect prompt until a source is set.",
740
+ capabilities: ["portfolio", "quote-stream"],
741
+ account: true,
742
+ schema: z3.object({ ...portfolioConfigShape })
743
+ });
744
+ var portfolioHoldingsMeta = defineFrameMeta({
745
+ name: "portfolio-holdings",
746
+ category: "portfolio",
747
+ iconUrl: widgetIcon("portfolio-holdings"),
748
+ layout: { w: 4, h: 4, minW: 3, minH: 3 },
749
+ description: "Table of your connected portfolio's positions \u2014 asset, amount, live USD value, share of total, 24h change. Source is a connected Binance account (read-only key, in-app) or a public on-chain wallet address. Renders a connect prompt until a source is set.",
750
+ capabilities: ["portfolio", "quote-stream"],
751
+ account: true,
752
+ schema: z3.object({ ...portfolioConfigShape })
753
+ });
577
754
  var newsFeedMeta = defineFrameMeta({
578
755
  name: "news-feed",
756
+ category: "sentiment",
757
+ iconUrl: widgetIcon("news-feed"),
579
758
  layout: { w: 4, h: 4, minW: 3, minH: 3 },
580
759
  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
760
  capabilities: ["news"],
@@ -598,6 +777,8 @@ var newsFeedMeta = defineFrameMeta({
598
777
  });
599
778
  var dexVolumeTreemapMeta = defineFrameMeta({
600
779
  name: "dex-volume-treemap",
780
+ category: "crypto",
781
+ iconUrl: widgetIcon("dex-volume-treemap"),
601
782
  layout: { w: 6, h: 4, minW: 3, minH: 3 },
602
783
  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
784
  capabilities: ["dex-volume"],
@@ -608,6 +789,8 @@ var dexVolumeTreemapMeta = defineFrameMeta({
608
789
  });
609
790
  var dexVolumeChartMeta = defineFrameMeta({
610
791
  name: "dex-volume-chart",
792
+ category: "crypto",
793
+ iconUrl: widgetIcon("dex-volume-chart"),
611
794
  layout: { w: 6, h: 3, minW: 3, minH: 2 },
612
795
  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
796
  capabilities: ["dex-volume"],
@@ -621,6 +804,8 @@ var dexVolumeChartMeta = defineFrameMeta({
621
804
  });
622
805
  var protocolTvlTreemapMeta = defineFrameMeta({
623
806
  name: "protocol-tvl-treemap",
807
+ category: "crypto",
808
+ iconUrl: widgetIcon("protocol-tvl-treemap"),
624
809
  layout: { w: 6, h: 4, minW: 3, minH: 3 },
625
810
  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
811
  capabilities: ["protocol-tvl"],
@@ -631,6 +816,8 @@ var protocolTvlTreemapMeta = defineFrameMeta({
631
816
  });
632
817
  var protocolTvlChartMeta = defineFrameMeta({
633
818
  name: "protocol-tvl-chart",
819
+ category: "crypto",
820
+ iconUrl: widgetIcon("protocol-tvl-chart"),
634
821
  layout: { w: 6, h: 3, minW: 3, minH: 2 },
635
822
  description: "Multi-series line chart of total value locked (TVL) for several DeFi protocols over a lookback window. Data from DeFiLlama (daily granularity).",
636
823
  capabilities: ["protocol-tvl"],
@@ -644,6 +831,8 @@ var protocolTvlChartMeta = defineFrameMeta({
644
831
  });
645
832
  var protocolFeesTreemapMeta = defineFrameMeta({
646
833
  name: "protocol-fees-treemap",
834
+ category: "crypto",
835
+ iconUrl: widgetIcon("protocol-fees-treemap"),
647
836
  layout: { w: 6, h: 4, minW: 3, minH: 3 },
648
837
  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
838
  capabilities: ["protocol-fees"],
@@ -654,18 +843,22 @@ var protocolFeesTreemapMeta = defineFrameMeta({
654
843
  });
655
844
  var marketCapTreemapMeta = defineFrameMeta({
656
845
  name: "market-cap-treemap",
846
+ category: "crypto",
847
+ iconUrl: widgetIcon("market-cap-treemap"),
657
848
  layout: { w: 6, h: 4, minW: 3, minH: 3 },
658
849
  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
850
  capabilities: ["coin-markets"],
660
851
  source: SOURCES.coingecko,
661
852
  schema: z3.object({
662
- topN: z3.number().int().min(5).max(50).default(20).describe(
853
+ topN: z3.number().int().min(5).max(50).default(12).describe(
663
854
  "How many of the largest coins by market cap to show (up to 50)."
664
855
  )
665
856
  })
666
857
  });
667
858
  var openInterestMeta = defineFrameMeta({
668
859
  name: "open-interest",
860
+ category: "derivatives",
861
+ iconUrl: widgetIcon("open-interest"),
669
862
  layout: { w: 4, h: 3, minW: 3, minH: 2 },
670
863
  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
864
  capabilities: ["open-interest"],
@@ -678,6 +871,8 @@ var openInterestMeta = defineFrameMeta({
678
871
  });
679
872
  var snakeMeta = defineFrameMeta({
680
873
  name: "snake",
874
+ category: "games",
875
+ iconUrl: widgetIcon("snake"),
681
876
  layout: { w: 4, h: 4, minW: 3, minH: 3 },
682
877
  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
878
  capabilities: [],
@@ -685,6 +880,8 @@ var snakeMeta = defineFrameMeta({
685
880
  });
686
881
  var flappyBirdMeta = defineFrameMeta({
687
882
  name: "flappy-bird",
883
+ category: "games",
884
+ iconUrl: widgetIcon("flappy-bird"),
688
885
  layout: { w: 4, h: 4, minW: 3, minH: 3 },
689
886
  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
887
  capabilities: [],
@@ -692,6 +889,8 @@ var flappyBirdMeta = defineFrameMeta({
692
889
  });
693
890
  var videoMeta = defineFrameMeta({
694
891
  name: "video",
892
+ category: "layout",
893
+ iconUrl: widgetIcon("video"),
695
894
  layout: { w: 4, h: 3, minW: 2, minH: 2 },
696
895
  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
896
  capabilities: [],
@@ -704,6 +903,8 @@ var videoMeta = defineFrameMeta({
704
903
  });
705
904
  var drawdyMeta = defineFrameMeta({
706
905
  name: "drawdy",
906
+ category: "layout",
907
+ iconUrl: widgetIcon("drawdy"),
707
908
  layout: { w: 8, h: 6, minW: 2, minH: 2 },
708
909
  description: "Embeds drawdy.io as an interactive whiteboard canvas. No configuration needed.",
709
910
  capabilities: [],
@@ -711,6 +912,8 @@ var drawdyMeta = defineFrameMeta({
711
912
  });
712
913
  var countdownMeta = defineFrameMeta({
713
914
  name: "countdown",
915
+ category: "tools",
916
+ iconUrl: widgetIcon("countdown"),
714
917
  layout: { w: 3, h: 2, minW: 2, minH: 1 },
715
918
  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
919
  capabilities: [],
@@ -726,6 +929,8 @@ var countdownMeta = defineFrameMeta({
726
929
  });
727
930
  var linkGridMeta = defineFrameMeta({
728
931
  name: "link-grid",
932
+ category: "tools",
933
+ iconUrl: widgetIcon("link-grid"),
729
934
  layout: { w: 3, h: 2, minW: 2, minH: 1 },
730
935
  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
936
  capabilities: [],
@@ -755,6 +960,8 @@ var linkGridMeta = defineFrameMeta({
755
960
  });
756
961
  var calculatorMeta = defineFrameMeta({
757
962
  name: "calculator",
963
+ category: "tools",
964
+ iconUrl: widgetIcon("calculator"),
758
965
  layout: { w: 3, h: 3, minW: 2, minH: 2 },
759
966
  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
967
  capabilities: [],
@@ -770,6 +977,8 @@ var calculatorMeta = defineFrameMeta({
770
977
  });
771
978
  var quoteMeta = defineFrameMeta({
772
979
  name: "quote",
980
+ category: "layout",
981
+ iconUrl: widgetIcon("quote"),
773
982
  layout: { w: 4, h: 2, minW: 2, minH: 1 },
774
983
  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
984
  capabilities: [],
@@ -788,6 +997,8 @@ var quoteMeta = defineFrameMeta({
788
997
  });
789
998
  var dividerMeta = defineFrameMeta({
790
999
  name: "divider",
1000
+ category: "layout",
1001
+ iconUrl: widgetIcon("divider"),
791
1002
  layout: { w: 12, h: 1, minW: 1, minH: 1 },
792
1003
  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
1004
  capabilities: [],
@@ -802,8 +1013,228 @@ var dividerMeta = defineFrameMeta({
802
1013
  style: z3.enum(["solid", "dashed", "dotted"]).default("solid").describe("Line style.")
803
1014
  })
804
1015
  });
1016
+ var diceMeta = defineFrameMeta({
1017
+ name: "dice",
1018
+ category: "tools",
1019
+ iconUrl: widgetIcon("dice"),
1020
+ layout: { w: 2, h: 2, minW: 1, minH: 1 },
1021
+ description: "A click-to-decide widget \u2014 a random decision-maker with no data provider. Flip a coin (heads/tails), roll a die (1\u20136), or pick at random from your own list of options. Click the surface to re-roll. Use it to break a tie, pick what to trade, or settle any small decision.",
1022
+ capabilities: [],
1023
+ schema: z3.object({
1024
+ mode: z3.enum(["coin", "dice", "list"]).default("coin").describe(
1025
+ "coin = heads/tails, dice = 1\u20136, list = random pick from options."
1026
+ ),
1027
+ options: z3.array(z3.string()).default(["Yes", "No"]).describe("Choices used in list mode."),
1028
+ label: z3.string().default("").describe("Optional caption, e.g. the question being decided.")
1029
+ })
1030
+ });
1031
+ var riskRewardMeta = defineFrameMeta({
1032
+ name: "risk-reward",
1033
+ category: "tools",
1034
+ iconUrl: widgetIcon("risk-reward"),
1035
+ layout: { w: 3, h: 3, minW: 2, minH: 2 },
1036
+ description: "Risk:reward planner. Enter entry, stop-loss and profit-target prices; it computes the per-unit risk and reward, their percentages of entry, and the resulting R:R ratio, shown large above a two-segment bar (red risk leg vs green reward leg, sized to scale). Pure client-side math \u2014 no data provider. Complements the calculator frame by adding the target/reward leg the position sizer leaves out.",
1037
+ capabilities: [],
1038
+ schema: z3.object({
1039
+ entry: z3.number().default(100).describe("Planned entry price."),
1040
+ stop: z3.number().default(95).describe("Stop-loss price."),
1041
+ target: z3.number().default(115).describe("Profit target price."),
1042
+ direction: z3.enum(["long", "short"]).default("long").describe(
1043
+ "Trade direction. Long expects stop < entry < target; short expects target < entry < stop \u2014 used for labels and to flag a mismatched setup."
1044
+ ),
1045
+ label: z3.string().default("").describe("Optional caption.")
1046
+ })
1047
+ });
1048
+ var marqueeMeta = defineFrameMeta({
1049
+ name: "marquee",
1050
+ category: "layout",
1051
+ iconUrl: widgetIcon("marquee"),
1052
+ layout: { w: 6, h: 1, minW: 2, minH: 1 },
1053
+ description: "A chrome-less scrolling banner that glides custom text continuously right-to-left across the frame (think stadium ticker / news crawl). Renders with no card \u2014 it fills the whole frame. Use for a slogan, a reminder, or a hype line. Needs no data provider.",
1054
+ capabilities: [],
1055
+ chrome: "bare",
1056
+ schema: z3.object({
1057
+ text: z3.string().default("LFG").describe("The text that scrolls across the banner."),
1058
+ speed: z3.enum(["slow", "normal", "fast"]).default("normal").describe("Scroll speed."),
1059
+ accent: z3.boolean().default(true).describe("Tint the text with the dashboard accent color.")
1060
+ })
1061
+ });
1062
+ var stopwatchMeta = defineFrameMeta({
1063
+ name: "stopwatch",
1064
+ layout: { w: 3, h: 2, minW: 2, minH: 1 },
1065
+ category: "tools",
1066
+ iconUrl: widgetIcon("stopwatch"),
1067
+ description: "A count-up stopwatch \u2014 time-in-trade, a focus session, how long a setup has been live. Start / Pause / Reset, ticking up in H:MM:SS, and it persists across reloads (the running state is saved into the dashboard, so it keeps counting where it left off). Runs entirely client-side \u2014 needs no data provider.",
1068
+ capabilities: [],
1069
+ schema: z3.object({
1070
+ label: z3.string().default("Session").describe("Caption above the timer."),
1071
+ startedAt: z3.number().default(0).describe(
1072
+ "Epoch ms when the timer was last started; 0 = paused. Persisted automatically by the frame."
1073
+ ),
1074
+ accumulatedMs: z3.number().default(0).describe(
1075
+ "Milliseconds banked before the current run. Persisted automatically by the frame."
1076
+ )
1077
+ })
1078
+ });
1079
+ var sessionProgressMeta = defineFrameMeta({
1080
+ name: "session-progress",
1081
+ category: "tools",
1082
+ iconUrl: widgetIcon("session-progress"),
1083
+ layout: { w: 3, h: 2, minW: 2, minH: 1 },
1084
+ description: "A horizontal progress bar showing how far through today's trading session an exchange is \u2014 fills from open to close with a percent readout, and a 'closes in \u2026' / 'opens in \u2026' countdown. Pick any exchange code (NYSE, NASDAQ, LSE, TSX, B3, \u2026); sessions are computed client-side from the exchange's timezone and hours, so it needs no data provider.",
1085
+ capabilities: [],
1086
+ schema: z3.object({
1087
+ exchange: z3.string().default("NYSE").describe("Exchange code: NYSE, NASDAQ, LSE, TSX, B3, \u2026"),
1088
+ label: z3.string().default("").describe("Optional caption shown by the exchange code."),
1089
+ showCountdown: z3.boolean().default(true).describe("Show the time-to-open / time-to-close countdown.")
1090
+ })
1091
+ });
1092
+ var holidayCalendarMeta = defineFrameMeta({
1093
+ name: "holiday-calendar",
1094
+ category: "tools",
1095
+ iconUrl: widgetIcon("holiday-calendar"),
1096
+ layout: { w: 3, h: 4, minW: 2, minH: 2 },
1097
+ description: "Upcoming market holidays (full closures) for a chosen exchange \u2014 the next few dates with their weekday and a countdown ('in 9d'). Pick any exchange code (NYSE, NASDAQ, LSE, TSX, B3, \u2026); dates come from a bundled holiday table and are computed client-side, so it needs no data provider. Note: the bundled table currently covers 2026.",
1098
+ capabilities: [],
1099
+ schema: z3.object({
1100
+ exchange: z3.string().default("NYSE").describe("Exchange code: NYSE, NASDAQ, LSE, TSX, B3, \u2026"),
1101
+ count: z3.number().int().min(1).max(20).default(5).describe("How many upcoming holidays to list."),
1102
+ label: z3.string().default("").describe("Optional caption shown above the list.")
1103
+ })
1104
+ });
1105
+ var dayMeterMeta = defineFrameMeta({
1106
+ name: "day-meter",
1107
+ category: "tools",
1108
+ iconUrl: widgetIcon("day-meter"),
1109
+ layout: { w: 4, h: 2, minW: 2, minH: 1 },
1110
+ description: "A strip of the current week's days for a chosen exchange \u2014 today highlighted, market holidays flagged in amber, and (optionally) non-trading days greyed. Computed client-side from the exchange's trading days + a bundled holiday table; needs no data provider.",
1111
+ capabilities: [],
1112
+ schema: z3.object({
1113
+ exchange: z3.string().default("NYSE").describe("Exchange code: NYSE, NASDAQ, LSE, TSX, B3, \u2026"),
1114
+ weekdaysOnly: z3.boolean().default(true).describe(
1115
+ "Show only the exchange's trading days; off shows the full 7-day week with weekends greyed."
1116
+ ),
1117
+ label: z3.string().default("").describe("Optional caption shown by the strip.")
1118
+ })
1119
+ });
1120
+ var returnsProjectorMeta = defineFrameMeta({
1121
+ name: "returns-projector",
1122
+ category: "tools",
1123
+ iconUrl: widgetIcon("returns-projector"),
1124
+ layout: { w: 3, h: 4, minW: 2, minH: 3 },
1125
+ description: "A compound-growth projector \u2014 enter a starting principal, a percent return per period, the number of periods, and an optional per-period contribution; it charts the projected balance curve and shows the ending value and total gain. Pure client-side math, no data provider; complements the position-size/risk `calculator`.",
1126
+ capabilities: [],
1127
+ schema: z3.object({
1128
+ principal: z3.number().default(1e3).describe("Starting balance."),
1129
+ ratePct: z3.number().default(5).describe("Return per period, in percent (e.g. 5 = 5%)."),
1130
+ periods: z3.number().int().min(1).max(600).default(12).describe("Number of compounding periods to project."),
1131
+ contribution: z3.number().default(0).describe("Amount added at the end of each period."),
1132
+ label: z3.string().default("").describe("Optional caption.")
1133
+ })
1134
+ });
1135
+ var breakevenMeta = defineFrameMeta({
1136
+ name: "breakeven",
1137
+ category: "tools",
1138
+ iconUrl: widgetIcon("breakeven"),
1139
+ layout: { w: 3, h: 4, minW: 2, minH: 2 },
1140
+ description: "A break-even / average-cost calculator \u2014 add your fills (price + size) and it computes the size-weighted average entry; set an optional current price to see the unrealized P&L %. Pure client-side math, no data provider.",
1141
+ capabilities: [],
1142
+ schema: z3.object({
1143
+ fills: z3.array(
1144
+ z3.object({
1145
+ price: z3.number().describe("Fill price."),
1146
+ size: z3.number().describe("Fill size, in units.")
1147
+ })
1148
+ ).default([{ price: 100, size: 1 }]).describe(
1149
+ "Your entry fills; their size-weighted average is the break-even."
1150
+ ),
1151
+ currentPrice: z3.number().default(0).describe(
1152
+ "Optional current price; greater than 0 shows the unrealized P&L %."
1153
+ ),
1154
+ label: z3.string().default("").describe("Optional caption.")
1155
+ })
1156
+ });
1157
+ var checklistMeta = defineFrameMeta({
1158
+ name: "checklist",
1159
+ category: "tools",
1160
+ iconUrl: widgetIcon("checklist"),
1161
+ layout: { w: 3, h: 3, minW: 2, minH: 2 },
1162
+ description: "A tickable checklist \u2014 a pre-trade routine, a daily ritual, anything. Tap items to check them off; the checked state persists across reloads (saved into the dashboard). Client-side only, no data provider.",
1163
+ capabilities: [],
1164
+ schema: z3.object({
1165
+ title: z3.string().default("Pre-trade checklist").describe("Heading shown above the list."),
1166
+ items: z3.array(z3.string()).default([
1167
+ "Trend & bias aligned",
1168
+ "Stop level set",
1169
+ "Risk sized correctly"
1170
+ ]).describe("The checklist items, top to bottom."),
1171
+ checked: z3.array(z3.boolean()).default([]).describe(
1172
+ "Per-item checked state (by index); persisted automatically by the frame."
1173
+ )
1174
+ })
1175
+ });
1176
+ var pomodoroMeta = defineFrameMeta({
1177
+ name: "pomodoro",
1178
+ category: "tools",
1179
+ iconUrl: widgetIcon("pomodoro"),
1180
+ layout: { w: 3, h: 3, minW: 2, minH: 2 },
1181
+ description: "A Pomodoro focus timer \u2014 alternating work and break intervals with Start / Pause / Reset and a cycle counter, counting down in MM:SS. Runs entirely client-side with no data provider; timer state is in-session (not persisted).",
1182
+ capabilities: [],
1183
+ schema: z3.object({
1184
+ workMin: z3.number().min(1).max(180).default(25).describe("Work interval length, in minutes."),
1185
+ breakMin: z3.number().min(1).max(120).default(5).describe("Break interval length, in minutes."),
1186
+ cycles: z3.number().int().min(1).max(20).default(4).describe("Work/break cycles before the counter loops."),
1187
+ label: z3.string().default("").describe("Optional caption.")
1188
+ })
1189
+ });
1190
+ var rulesCardMeta = defineFrameMeta({
1191
+ name: "rules-card",
1192
+ category: "layout",
1193
+ iconUrl: widgetIcon("rules-card"),
1194
+ layout: { w: 3, h: 3, minW: 2, minH: 2 },
1195
+ description: "A pinned, auto-numbered list of your trading rules (or any principles) \u2014 always fully visible, unlike the rotating `quote` frame. Static text, client-side, no data provider.",
1196
+ capabilities: [],
1197
+ schema: z3.object({
1198
+ title: z3.string().default("My rules").describe("Heading shown above the list."),
1199
+ rules: z3.array(z3.string()).default([
1200
+ "Cut losers fast, let winners run",
1201
+ "No trade without a stop",
1202
+ "One setup at a time"
1203
+ ]).describe("The rules, in order; rendered as a numbered list.")
1204
+ })
1205
+ });
1206
+ var breathingMeta = defineFrameMeta({
1207
+ name: "breathing",
1208
+ category: "layout",
1209
+ iconUrl: widgetIcon("breathing"),
1210
+ chrome: "bare",
1211
+ layout: { w: 2, h: 2, minW: 1, minH: 1 },
1212
+ description: "A chrome-less breathing pacer \u2014 a circle that expands and contracts through configurable inhale / hold / exhale / hold phases to steady your breathing between trades. Renders with no card; client-side only, no data provider.",
1213
+ capabilities: [],
1214
+ schema: z3.object({
1215
+ inhale: z3.number().min(1).max(60).default(4).describe("Inhale seconds."),
1216
+ hold: z3.number().min(0).max(60).default(4).describe("Hold-after-inhale seconds."),
1217
+ exhale: z3.number().min(1).max(60).default(4).describe("Exhale seconds."),
1218
+ holdAfter: z3.number().min(0).max(60).default(4).describe("Hold-after-exhale seconds.")
1219
+ })
1220
+ });
1221
+ var spotifyEmbedMeta = defineFrameMeta({
1222
+ name: "spotify-embed",
1223
+ category: "layout",
1224
+ iconUrl: widgetIcon("spotify-embed"),
1225
+ layout: { w: 3, h: 4, minW: 2, minH: 2 },
1226
+ description: "Embeds a Spotify track, album, playlist, artist, or show from its public open.spotify.com share link (same embed approach as the `video` frame), using Spotify's official keyless iframe player. Needs an internet connection to play.",
1227
+ capabilities: [],
1228
+ schema: z3.object({
1229
+ url: z3.string().default("https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M").describe(
1230
+ "A Spotify share URL (open.spotify.com/track|album|playlist|artist/\u2026)."
1231
+ ),
1232
+ compact: z3.boolean().default(false).describe("Use Spotify's compact (single-row) player height.")
1233
+ })
1234
+ });
805
1235
  var btcFeesMeta = defineFrameMeta({
806
1236
  name: "btc-fees",
1237
+ category: "bitcoin",
807
1238
  iconUrl: widgetIcon("btc-fees"),
808
1239
  layout: { w: 3, h: 2, minW: 2, minH: 2 },
809
1240
  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.",
@@ -817,6 +1248,7 @@ var btcFeesMeta = defineFrameMeta({
817
1248
  });
818
1249
  var btcMempoolMeta = defineFrameMeta({
819
1250
  name: "btc-mempool",
1251
+ category: "bitcoin",
820
1252
  iconUrl: widgetIcon("btc-mempool"),
821
1253
  layout: { w: 5, h: 3, minW: 3, minH: 2 },
822
1254
  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.",
@@ -830,6 +1262,7 @@ var btcMempoolMeta = defineFrameMeta({
830
1262
  });
831
1263
  var btcBlocksMeta = defineFrameMeta({
832
1264
  name: "btc-blocks",
1265
+ category: "bitcoin",
833
1266
  iconUrl: widgetIcon("btc-blocks"),
834
1267
  layout: { w: 5, h: 4, minW: 3, minH: 3 },
835
1268
  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.",
@@ -841,6 +1274,7 @@ var btcBlocksMeta = defineFrameMeta({
841
1274
  });
842
1275
  var btcHashrateMeta = defineFrameMeta({
843
1276
  name: "btc-hashrate",
1277
+ category: "bitcoin",
844
1278
  iconUrl: widgetIcon("btc-hashrate"),
845
1279
  layout: { w: 6, h: 3, minW: 3, minH: 2 },
846
1280
  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.",
@@ -852,6 +1286,7 @@ var btcHashrateMeta = defineFrameMeta({
852
1286
  });
853
1287
  var btcDifficultyMeta = defineFrameMeta({
854
1288
  name: "btc-difficulty",
1289
+ category: "bitcoin",
855
1290
  iconUrl: widgetIcon("btc-difficulty"),
856
1291
  layout: { w: 4, h: 3, minW: 3, minH: 2 },
857
1292
  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.",
@@ -865,6 +1300,7 @@ var btcDifficultyMeta = defineFrameMeta({
865
1300
  });
866
1301
  var miningPoolsMeta = defineFrameMeta({
867
1302
  name: "mining-pools",
1303
+ category: "bitcoin",
868
1304
  iconUrl: widgetIcon("mining-pools"),
869
1305
  layout: { w: 6, h: 4, minW: 3, minH: 3 },
870
1306
  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.",
@@ -879,6 +1315,7 @@ var miningPoolsMeta = defineFrameMeta({
879
1315
  });
880
1316
  var lightningStatsMeta = defineFrameMeta({
881
1317
  name: "lightning-stats",
1318
+ category: "bitcoin",
882
1319
  iconUrl: widgetIcon("lightning-stats"),
883
1320
  layout: { w: 4, h: 3, minW: 3, minH: 2 },
884
1321
  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.",
@@ -892,6 +1329,7 @@ var lightningStatsMeta = defineFrameMeta({
892
1329
  });
893
1330
  var optionsPutCallMeta = defineFrameMeta({
894
1331
  name: "options-put-call",
1332
+ category: "derivatives",
895
1333
  iconUrl: widgetIcon("options-put-call"),
896
1334
  layout: { w: 4, h: 3, minW: 3, minH: 2 },
897
1335
  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.",
@@ -908,6 +1346,7 @@ var optionsPutCallMeta = defineFrameMeta({
908
1346
  });
909
1347
  var optionsIvMeta = defineFrameMeta({
910
1348
  name: "options-iv",
1349
+ category: "derivatives",
911
1350
  iconUrl: widgetIcon("options-iv"),
912
1351
  layout: { w: 6, h: 3, minW: 3, minH: 2 },
913
1352
  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.",
@@ -920,6 +1359,7 @@ var optionsIvMeta = defineFrameMeta({
920
1359
  });
921
1360
  var optionsOiStrikeMeta = defineFrameMeta({
922
1361
  name: "options-oi-strike",
1362
+ category: "derivatives",
923
1363
  iconUrl: widgetIcon("options-oi-strike"),
924
1364
  layout: { w: 6, h: 4, minW: 4, minH: 3 },
925
1365
  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.",
@@ -934,6 +1374,7 @@ var optionsOiStrikeMeta = defineFrameMeta({
934
1374
  });
935
1375
  var coinMoversMeta = defineFrameMeta({
936
1376
  name: "coin-movers",
1377
+ category: "markets",
937
1378
  iconUrl: widgetIcon("coin-movers"),
938
1379
  layout: { w: 5, h: 4, minW: 3, minH: 3 },
939
1380
  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.",
@@ -951,7 +1392,9 @@ var coinMoversMeta = defineFrameMeta({
951
1392
  });
952
1393
  var frameMetas = [
953
1394
  newsFeedMeta,
954
- allocationMeta,
1395
+ portfolioValueMeta,
1396
+ portfolioAllocationMeta,
1397
+ portfolioHoldingsMeta,
955
1398
  bitcoinDominanceMeta,
956
1399
  clockMeta,
957
1400
  dailyAnalysisMeta,
@@ -1053,7 +1496,19 @@ function skeleton(title, author) {
1053
1496
  projectId: "YrTzGatwjK7EoFpCSfgZ",
1054
1497
  opacity: 1
1055
1498
  },
1056
- theme: { accentHue: 242, accentSat: 90 },
1499
+ theme: {
1500
+ accentHue: 242,
1501
+ accentSat: 90,
1502
+ baseHue: 233,
1503
+ baseSat: 20,
1504
+ upColor: "#3fd08f",
1505
+ downColor: "#ff6b81"
1506
+ },
1507
+ typography: {
1508
+ fontFamily: "sans",
1509
+ numericStyle: "proportional",
1510
+ scale: 1
1511
+ },
1057
1512
  appearance: {
1058
1513
  radius: 18,
1059
1514
  borderStrength: 0.22,
@@ -1148,6 +1603,12 @@ function lintSpec(spec) {
1148
1603
  frameId: instance.id,
1149
1604
  message: `overflows the grid: x(${instance.position.x}) + w(${instance.position.w}) > ${spec.grid.columns} columns`
1150
1605
  });
1606
+ const horizontal = instance.layouts?.["flow-horizontal"];
1607
+ if (horizontal && horizontal.y + horizontal.h > spec.grid.rows)
1608
+ issues.push({
1609
+ frameId: instance.id,
1610
+ message: `horizontal layout overflows: y(${horizontal.y}) + h(${horizontal.h}) > ${spec.grid.rows} rows`
1611
+ });
1151
1612
  }
1152
1613
  for (let i = 0; i < spec.frames.length; i++) {
1153
1614
  for (let j = i + 1; j < spec.frames.length; j++) {
@@ -1167,7 +1628,7 @@ function lintSpec(spec) {
1167
1628
  // src/serve.ts
1168
1629
  import { existsSync as existsSync2, statSync as statSync2 } from "fs";
1169
1630
  import { createServer } from "http";
1170
- import { join as join3, resolve as resolve2 } from "path";
1631
+ import { join as join4, resolve as resolve2 } from "path";
1171
1632
  import { fileURLToPath } from "url";
1172
1633
  import sirv from "sirv";
1173
1634
 
@@ -1178,6 +1639,8 @@ import { readFile, writeFile } from "fs/promises";
1178
1639
  var DASHBOARD_READ_ROUTE = "/__zframes/dashboard.json";
1179
1640
  var DASHBOARD_WRITE_ROUTE = "/__zframes/dashboard";
1180
1641
  var DASHBOARD_PROXY_ROUTE = "/__zframes/proxy";
1642
+ var ACCOUNT_PORTFOLIO_ROUTE = "/__zframes/account/portfolio";
1643
+ var ACCOUNT_CREDENTIALS_ROUTE = "/__zframes/account/credentials";
1181
1644
  var AGENTS_LIST_ROUTE = "/__zframes/agents";
1182
1645
  var ASK_ROUTE = "/__zframes/ask";
1183
1646
 
@@ -1244,8 +1707,8 @@ function handleSpecWrite(req, res, absFile) {
1244
1707
  req.on("end", async () => {
1245
1708
  if (aborted) return;
1246
1709
  try {
1247
- const json = JSON.parse(body);
1248
- await writeFile(absFile, `${JSON.stringify(json, null, 2)}
1710
+ const json2 = JSON.parse(body);
1711
+ await writeFile(absFile, `${JSON.stringify(json2, null, 2)}
1249
1712
  `, "utf8");
1250
1713
  res.statusCode = 200;
1251
1714
  res.setHeader("content-type", "application/json");
@@ -1318,14 +1781,59 @@ import { tmpdir } from "os";
1318
1781
  import { dirname as dirname2, join as join2 } from "path";
1319
1782
  var MAX_BODY_BYTES2 = 64e3;
1320
1783
  var RUN_TIMEOUT_MS = 12e4;
1784
+ var MAX_CONTEXT_CHARS = 12e3;
1785
+ var MAX_HISTORY_TURNS = 6;
1786
+ var MAX_HISTORY_CHARS = 600;
1787
+ function tryParse(line) {
1788
+ const s = line.trim();
1789
+ if (!s) return null;
1790
+ try {
1791
+ return JSON.parse(s);
1792
+ } catch {
1793
+ return null;
1794
+ }
1795
+ }
1796
+ function claudeDelta(line) {
1797
+ const o = tryParse(line);
1798
+ if (o?.type !== "stream_event") return null;
1799
+ const delta = o.event?.type === "content_block_delta" ? o.event.delta : void 0;
1800
+ return delta?.type === "text_delta" && typeof delta.text === "string" ? delta.text : null;
1801
+ }
1802
+ function claudeResult(stdout) {
1803
+ let result = null;
1804
+ let deltas = "";
1805
+ for (const line of stdout.split("\n")) {
1806
+ const o = tryParse(line);
1807
+ if (!o) continue;
1808
+ if (o.type === "result" && typeof o.result === "string") {
1809
+ result = o.result;
1810
+ continue;
1811
+ }
1812
+ if (o.type === "stream_event" && o.event?.type === "content_block_delta" && o.event.delta?.type === "text_delta" && typeof o.event.delta.text === "string") {
1813
+ deltas += o.event.delta.text;
1814
+ }
1815
+ }
1816
+ return (result ?? deltas).trim();
1817
+ }
1321
1818
  var RUNNERS = [
1322
1819
  {
1323
1820
  id: "claude",
1324
1821
  label: "Claude",
1325
1822
  bin: "claude",
1326
- // -p/--print is non-interactive; text format prints just the answer.
1327
- buildArgs: (prompt) => ["-p", prompt, "--output-format", "text"],
1328
- readResult: async (stdout) => stdout.trim()
1823
+ // -p/--print is non-interactive. stream-json emits NDJSON events as the
1824
+ // answer is generated; --verbose is required alongside it under -p, and
1825
+ // --include-partial-messages adds the token-level `content_block_delta`
1826
+ // events we relay live. The canonical answer is the closing `result` event.
1827
+ buildArgs: (prompt) => [
1828
+ "-p",
1829
+ prompt,
1830
+ "--output-format",
1831
+ "stream-json",
1832
+ "--verbose",
1833
+ "--include-partial-messages"
1834
+ ],
1835
+ parseDelta: claudeDelta,
1836
+ readResult: async (stdout) => claudeResult(stdout)
1329
1837
  },
1330
1838
  {
1331
1839
  id: "codex",
@@ -1382,7 +1890,7 @@ function detectAgents() {
1382
1890
  }
1383
1891
  return detected;
1384
1892
  }
1385
- async function buildPrompt(specFile, question) {
1893
+ async function buildPrompt(specFile, question, clientContext, catalogue, history) {
1386
1894
  let title = "a live market dashboard";
1387
1895
  const symbols = /* @__PURE__ */ new Set();
1388
1896
  try {
@@ -1397,13 +1905,40 @@ async function buildPrompt(specFile, question) {
1397
1905
  }
1398
1906
  } catch {
1399
1907
  }
1400
- const universe = symbols.size ? [...symbols].join(", ") : "no specific symbols";
1401
- return `You are zAI, a market assistant embedded in a live dashboard titled "${title}". The symbols on screen right now are: ${universe}. Answer the user's question in 2\u20134 sentences of plain text \u2014 no markdown headings, no preamble, no tool use, just the answer.
1908
+ const trimmed = clientContext?.trim();
1909
+ const grounding = trimmed ? `Here is what the user is looking at on their dashboard "${title}" right now (live values captured from the screen):
1910
+
1911
+ ${trimmed.slice(
1912
+ 0,
1913
+ MAX_CONTEXT_CHARS
1914
+ )}` : `The user's dashboard is titled "${title}". The symbols on screen right now are: ${symbols.size ? [...symbols].join(", ") : "no specific symbols"}.`;
1915
+ const trimmedCatalogue = catalogue?.trim();
1916
+ const frameCatalogue = trimmedCatalogue ? `The frames a user can add in zframes (name \u2014 what it shows), by family:
1917
+ ${trimmedCatalogue}
1918
+
1919
+ ` : "";
1920
+ const recent = (history ?? []).filter((m) => m.text.trim()).slice(-MAX_HISTORY_TURNS);
1921
+ const transcript = recent.length ? `Conversation so far (most recent last):
1922
+ ${recent.map(
1923
+ (m) => `${m.role === "user" ? "User" : "zAI"}: ${m.text.replace(/\s+/g, " ").trim().slice(0, MAX_HISTORY_CHARS)}`
1924
+ ).join("\n")}
1925
+
1926
+ ` : "";
1927
+ return (
1928
+ // Primer: brief the runner on what zframes is and how it works, so a
1929
+ // general-purpose agent answers as the embedded assistant rather than from
1930
+ // cold. Kept tight — the catalogue + live digest below carry the specifics.
1931
+ `You are zAI, the assistant built into zframes \u2014 a keyless, AI-personalizable live market dashboard for crypto and stocks. Users assemble their own dashboard from "frames": self-contained widgets for live prices, funding, open interest, fear & greed, TVL and on-chain activity, macro rates, news, even games. They arrange frames on a grid in "customise" mode \u2014 drag, resize, add, remove, and configure each \u2014 and edits save to a dashboard.json the runtime renders from. All data comes from free public APIs (no keys, no accounts); stocks are Hyperliquid HIP-3 equity perps (e.g. "xyz:TSLA") shown alongside crypto. Help the user read what's on their screen and the markets it tracks.
1932
+
1933
+ ${frameCatalogue}${grounding}
1934
+
1935
+ ${transcript}Answer the user's question in 2\u20134 sentences of plain text \u2014 no markdown headings, no preamble, no tool use, just the answer.
1402
1936
 
1403
- Question: ${question}`;
1937
+ Question: ${question}`
1938
+ );
1404
1939
  }
1405
1940
  var askCounter = 0;
1406
- function runAgent(runner, prompt, cwd) {
1941
+ function runAgent(runner, prompt, cwd, onDelta) {
1407
1942
  const outFile = join2(
1408
1943
  tmpdir(),
1409
1944
  `zframes-ask-${process.pid}-${++askCounter}.txt`
@@ -1411,13 +1946,17 @@ function runAgent(runner, prompt, cwd) {
1411
1946
  return new Promise((resolve4) => {
1412
1947
  let child;
1413
1948
  try {
1414
- child = spawn(runner.bin, runner.buildArgs(prompt, outFile), { cwd });
1949
+ child = spawn(runner.bin, runner.buildArgs(prompt, outFile), {
1950
+ cwd,
1951
+ stdio: ["ignore", "pipe", "pipe"]
1952
+ });
1415
1953
  } catch (error) {
1416
1954
  resolve4({ ok: false, error: String(error) });
1417
1955
  return;
1418
1956
  }
1419
1957
  let stdout = "";
1420
1958
  let stderr = "";
1959
+ let lineBuf = "";
1421
1960
  let settled = false;
1422
1961
  const finish = (r) => {
1423
1962
  if (settled) return;
@@ -1429,13 +1968,28 @@ function runAgent(runner, prompt, cwd) {
1429
1968
  child.kill("SIGKILL");
1430
1969
  finish({ ok: false, error: `${runner.label} timed out` });
1431
1970
  }, RUN_TIMEOUT_MS);
1432
- child.stdout?.on("data", (d) => stdout += d);
1971
+ const streaming = Boolean(onDelta && runner.parseDelta);
1972
+ const emit = (line) => {
1973
+ const delta = runner.parseDelta?.(line);
1974
+ if (delta) onDelta?.(delta);
1975
+ };
1976
+ child.stdout?.on("data", (d) => {
1977
+ stdout += d;
1978
+ if (!streaming) return;
1979
+ lineBuf += d;
1980
+ let nl;
1981
+ while ((nl = lineBuf.indexOf("\n")) !== -1) {
1982
+ emit(lineBuf.slice(0, nl));
1983
+ lineBuf = lineBuf.slice(nl + 1);
1984
+ }
1985
+ });
1433
1986
  child.stderr?.on("data", (d) => stderr += d);
1434
1987
  child.on(
1435
1988
  "error",
1436
1989
  (e) => finish({ ok: false, error: String(e.message) })
1437
1990
  );
1438
1991
  child.on("close", (code) => {
1992
+ if (streaming && lineBuf) emit(lineBuf);
1439
1993
  if (code !== 0) {
1440
1994
  finish({
1441
1995
  ok: false,
@@ -1460,7 +2014,7 @@ async function handleAgents(res) {
1460
2014
  JSON.stringify({ agents: agents.map(({ id, label }) => ({ id, label })) })
1461
2015
  );
1462
2016
  }
1463
- function handleAsk(req, res, specFile) {
2017
+ function handleAsk(req, res, specFile, catalogue) {
1464
2018
  if (req.method !== "POST") {
1465
2019
  res.statusCode = 405;
1466
2020
  res.end();
@@ -1485,42 +2039,292 @@ function handleAsk(req, res, specFile) {
1485
2039
  });
1486
2040
  req.on("end", async () => {
1487
2041
  if (aborted) return;
1488
- const reply = (status, payload) => {
2042
+ const replyJson = (status, payload) => {
1489
2043
  res.statusCode = status;
1490
2044
  res.setHeader("content-type", "application/json");
1491
2045
  res.end(JSON.stringify(payload));
1492
2046
  };
1493
2047
  let question;
1494
2048
  let requested;
2049
+ let clientContext;
2050
+ let history;
1495
2051
  try {
1496
2052
  const parsed = JSON.parse(body);
1497
2053
  if (typeof parsed.question !== "string" || !parsed.question.trim())
1498
2054
  throw new Error("missing question");
1499
2055
  question = parsed.question.trim();
1500
2056
  requested = typeof parsed.agent === "string" ? parsed.agent : void 0;
2057
+ clientContext = typeof parsed.context === "string" ? parsed.context : void 0;
2058
+ if (Array.isArray(parsed.history))
2059
+ history = parsed.history.filter(
2060
+ (m) => !!m && typeof m === "object" && (m.role === "user" || m.role === "zai") && typeof m.text === "string"
2061
+ ).slice(-MAX_HISTORY_TURNS);
1501
2062
  } catch (error) {
1502
- reply(400, { ok: false, error: String(error.message) });
2063
+ replyJson(400, { ok: false, error: String(error.message) });
1503
2064
  return;
1504
2065
  }
1505
2066
  const agents = await detectAgents();
1506
2067
  if (agents.length === 0) {
1507
- reply(503, {
2068
+ replyJson(503, {
1508
2069
  ok: false,
1509
2070
  error: "no agent CLI found \u2014 install claude, codex, or kimi"
1510
2071
  });
1511
2072
  return;
1512
2073
  }
1513
2074
  const runner = agents.find((a) => a.id === requested) ?? agents[0];
1514
- const prompt = await buildPrompt(specFile, question);
1515
- const result = await runAgent(runner, prompt, dirname2(specFile));
2075
+ const prompt = await buildPrompt(
2076
+ specFile,
2077
+ question,
2078
+ clientContext,
2079
+ catalogue,
2080
+ history
2081
+ );
2082
+ res.statusCode = 200;
2083
+ res.setHeader("content-type", "application/x-ndjson; charset=utf-8");
2084
+ res.setHeader("cache-control", "no-store");
2085
+ res.setHeader("x-accel-buffering", "no");
2086
+ const send = (event) => {
2087
+ try {
2088
+ res.write(`${JSON.stringify(event)}
2089
+ `);
2090
+ } catch {
2091
+ }
2092
+ };
2093
+ const result = await runAgent(
2094
+ runner,
2095
+ prompt,
2096
+ dirname2(specFile),
2097
+ (text) => send({ type: "delta", text })
2098
+ );
1516
2099
  if (result.ok)
1517
- reply(200, { ok: true, agent: runner.id, answer: result.answer });
1518
- else reply(502, { ok: false, agent: runner.id, error: result.error });
2100
+ send({ type: "done", agent: runner.id, answer: result.answer });
2101
+ else send({ type: "error", agent: runner.id, error: result.error });
2102
+ try {
2103
+ res.end();
2104
+ } catch {
2105
+ }
2106
+ });
2107
+ }
2108
+
2109
+ // ../core/src/account.ts
2110
+ import { createHmac } from "crypto";
2111
+ import { mkdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
2112
+ import { homedir } from "os";
2113
+ import { join as join3 } from "path";
2114
+ function json(res, status, body) {
2115
+ res.statusCode = status;
2116
+ res.setHeader("content-type", "application/json");
2117
+ res.setHeader("cache-control", "no-store");
2118
+ res.end(JSON.stringify(body));
2119
+ }
2120
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
2121
+ function isLocalRequest(req) {
2122
+ const hostname = String(req.headers.host ?? "").split(":")[0];
2123
+ if (!LOCAL_HOSTS.has(hostname)) return false;
2124
+ const origin = req.headers.origin;
2125
+ if (origin !== void 0) {
2126
+ try {
2127
+ if (!LOCAL_HOSTS.has(new URL(String(origin)).hostname)) return false;
2128
+ } catch {
2129
+ return false;
2130
+ }
2131
+ }
2132
+ return true;
2133
+ }
2134
+ var MAX_BODY_BYTES3 = 8192;
2135
+ function readBody(req, res) {
2136
+ return new Promise((resolveBody) => {
2137
+ let body = "";
2138
+ let aborted = false;
2139
+ req.on("data", (chunk) => {
2140
+ if (aborted) return;
2141
+ body += chunk;
2142
+ if (body.length > MAX_BODY_BYTES3) {
2143
+ aborted = true;
2144
+ json(res, 413, { ok: false, error: "body too large" });
2145
+ req.destroy();
2146
+ resolveBody(null);
2147
+ }
2148
+ });
2149
+ req.on("end", () => {
2150
+ if (!aborted) resolveBody(body);
2151
+ });
2152
+ });
2153
+ }
2154
+ var CRED_DIR = join3(homedir(), ".zframes");
2155
+ var CRED_FILE = join3(CRED_DIR, "credentials.json");
2156
+ async function readStore() {
2157
+ try {
2158
+ const parsed = JSON.parse(await readFile3(CRED_FILE, "utf8"));
2159
+ return parsed && typeof parsed === "object" ? parsed : {};
2160
+ } catch {
2161
+ return {};
2162
+ }
2163
+ }
2164
+ async function writeStore(store) {
2165
+ await mkdir(CRED_DIR, { recursive: true, mode: 448 });
2166
+ await writeFile2(CRED_FILE, `${JSON.stringify(store, null, 2)}
2167
+ `, {
2168
+ encoding: "utf8",
2169
+ mode: 384
2170
+ });
2171
+ }
2172
+ function envCredential(source) {
2173
+ const up = source.toUpperCase();
2174
+ const key = process.env[`ZFRAMES_${up}_KEY`];
2175
+ const secret = process.env[`ZFRAMES_${up}_SECRET`];
2176
+ return key && secret ? { key, secret } : null;
2177
+ }
2178
+ async function getCredential(source) {
2179
+ const store = await readStore();
2180
+ return store[source] ?? envCredential(source);
2181
+ }
2182
+ function maskKey(key) {
2183
+ return key.length <= 4 ? "\u2026" : `\u2026${key.slice(-4)}`;
2184
+ }
2185
+ var STABLES = /* @__PURE__ */ new Set([
2186
+ "USDT",
2187
+ "USDC",
2188
+ "BUSD",
2189
+ "DAI",
2190
+ "FDUSD",
2191
+ "TUSD",
2192
+ "USDP",
2193
+ "USD"
2194
+ ]);
2195
+ var BINANCE_BASE = "https://api.binance.com";
2196
+ var BINANCE_TIMEOUT_MS = 15e3;
2197
+ function signBinance(query, secret) {
2198
+ return createHmac("sha256", secret).update(query).digest("hex");
2199
+ }
2200
+ function binanceHoldings(data) {
2201
+ return (data.balances ?? []).map((b) => ({
2202
+ symbol: b.asset,
2203
+ amount: Number(b.free) + Number(b.locked)
2204
+ })).filter((b) => Number.isFinite(b.amount) && b.amount > 0).map((b) => ({
2205
+ symbol: b.symbol,
2206
+ amount: b.amount,
2207
+ valueUsd: STABLES.has(b.symbol.toUpperCase()) ? b.amount : void 0
2208
+ }));
2209
+ }
2210
+ async function binanceAccount(cred, nowMs) {
2211
+ const query = `timestamp=${nowMs}&recvWindow=5000`;
2212
+ const signature = signBinance(query, cred.secret);
2213
+ const res = await fetch(
2214
+ `${BINANCE_BASE}/api/v3/account?${query}&signature=${signature}`,
2215
+ {
2216
+ headers: { "X-MBX-APIKEY": cred.key },
2217
+ signal: AbortSignal.timeout(BINANCE_TIMEOUT_MS)
2218
+ }
2219
+ );
2220
+ if (!res.ok) {
2221
+ throw new Error(`binance ${res.status}`);
2222
+ }
2223
+ return res.json();
2224
+ }
2225
+ var binanceAdapter = {
2226
+ async verify(cred) {
2227
+ await binanceAccount(cred, Date.now());
2228
+ },
2229
+ async portfolio(cred) {
2230
+ const data = await binanceAccount(cred, Date.now());
2231
+ return {
2232
+ source: "binance",
2233
+ label: "Binance",
2234
+ holdings: binanceHoldings(data),
2235
+ asOf: Date.now()
2236
+ };
2237
+ }
2238
+ };
2239
+ var ADAPTERS = { binance: binanceAdapter };
2240
+ function sourceParam(req) {
2241
+ const query = (req.url ?? "").split("?")[1] ?? "";
2242
+ return new URLSearchParams(query).get("source") ?? "";
2243
+ }
2244
+ async function handleAccountPortfolio(req, res) {
2245
+ if (!isLocalRequest(req))
2246
+ return json(res, 403, { ok: false, error: "non-local origin" });
2247
+ if (req.method !== "GET" && req.method !== "HEAD")
2248
+ return json(res, 405, { ok: false, error: "GET only" });
2249
+ const source = sourceParam(req);
2250
+ const adapter = ADAPTERS[source];
2251
+ if (!adapter)
2252
+ return json(res, 400, { ok: false, error: `unknown source: ${source}` });
2253
+ const cred = await getCredential(source);
2254
+ if (!cred)
2255
+ return json(res, 401, {
2256
+ ok: false,
2257
+ connected: false,
2258
+ error: "not connected"
2259
+ });
2260
+ try {
2261
+ json(res, 200, await adapter.portfolio(cred));
2262
+ } catch (error) {
2263
+ json(res, 502, { ok: false, error: String(error) });
2264
+ }
2265
+ }
2266
+ async function handleAccountCredentials(req, res) {
2267
+ if (!isLocalRequest(req))
2268
+ return json(res, 403, { ok: false, error: "non-local origin" });
2269
+ const method = req.method ?? "GET";
2270
+ if (method === "GET") {
2271
+ const cred = await getCredential(sourceParam(req));
2272
+ return json(res, 200, {
2273
+ connected: !!cred,
2274
+ keyMasked: cred ? maskKey(cred.key) : null
2275
+ });
2276
+ }
2277
+ if (method !== "POST" && method !== "DELETE")
2278
+ return json(res, 405, { ok: false, error: "method not allowed" });
2279
+ if (!String(req.headers["content-type"] ?? "").includes("application/json"))
2280
+ return json(res, 415, {
2281
+ ok: false,
2282
+ error: "content-type must be application/json"
2283
+ });
2284
+ const raw = await readBody(req, res);
2285
+ if (raw === null) return;
2286
+ let parsed;
2287
+ try {
2288
+ parsed = JSON.parse(raw);
2289
+ } catch {
2290
+ return json(res, 400, { ok: false, error: "invalid json" });
2291
+ }
2292
+ const source = String(parsed.source ?? "");
2293
+ if (!ADAPTERS[source])
2294
+ return json(res, 400, { ok: false, error: `unknown source: ${source}` });
2295
+ if (method === "DELETE") {
2296
+ const store2 = await readStore();
2297
+ delete store2[source];
2298
+ await writeStore(store2);
2299
+ return json(res, 200, { ok: true, connected: false });
2300
+ }
2301
+ const key = String(parsed.key ?? "").trim();
2302
+ const secret = String(parsed.secret ?? "").trim();
2303
+ if (!key || !secret)
2304
+ return json(res, 400, { ok: false, error: "key and secret required" });
2305
+ try {
2306
+ await ADAPTERS[source].verify({ key, secret });
2307
+ } catch (error) {
2308
+ return json(res, 400, {
2309
+ ok: false,
2310
+ verified: false,
2311
+ error: `verification failed: ${String(error)}`
2312
+ });
2313
+ }
2314
+ const store = await readStore();
2315
+ store[source] = { key, secret };
2316
+ await writeStore(store);
2317
+ return json(res, 200, {
2318
+ ok: true,
2319
+ verified: true,
2320
+ connected: true,
2321
+ keyMasked: maskKey(key)
1519
2322
  });
1520
2323
  }
1521
2324
 
1522
2325
  // src/serve.ts
1523
2326
  var DEFAULT_PORT = 37263;
2327
+ var FRAME_CATALOGUE = catalogueSummary(frameMetas);
1524
2328
  function parseArgs2(args) {
1525
2329
  let file = "dashboard.json";
1526
2330
  let port = DEFAULT_PORT;
@@ -1563,7 +2367,7 @@ function serve(args) {
1563
2367
  }
1564
2368
  const userDir = resolve2(file, "..");
1565
2369
  const bundleDir = fileURLToPath(new URL("../runtime", import.meta.url));
1566
- if (!existsSync2(join3(bundleDir, "index.html"))) {
2370
+ if (!existsSync2(join4(bundleDir, "index.html"))) {
1567
2371
  console.error(`\u2717 runtime bundle missing at ${bundleDir}`);
1568
2372
  console.error(" run `pnpm build:cli` to build it.");
1569
2373
  return Promise.resolve(1);
@@ -1605,7 +2409,15 @@ function serve(args) {
1605
2409
  return;
1606
2410
  }
1607
2411
  if (path === ASK_ROUTE) {
1608
- handleAsk(req, res, file);
2412
+ handleAsk(req, res, file, FRAME_CATALOGUE);
2413
+ return;
2414
+ }
2415
+ if (path === ACCOUNT_PORTFOLIO_ROUTE) {
2416
+ void handleAccountPortfolio(req, res);
2417
+ return;
2418
+ }
2419
+ if (path === ACCOUNT_CREDENTIALS_ROUTE) {
2420
+ void handleAccountCredentials(req, res);
1609
2421
  return;
1610
2422
  }
1611
2423
  if (path === DASHBOARD_PROXY_ROUTE) {
@@ -2173,14 +2985,14 @@ function loadSpec(file) {
2173
2985
  console.error(`\u2717 cannot read ${file}`);
2174
2986
  return null;
2175
2987
  }
2176
- let json;
2988
+ let json2;
2177
2989
  try {
2178
- json = JSON.parse(raw);
2990
+ json2 = JSON.parse(raw);
2179
2991
  } catch (error) {
2180
2992
  console.error(`\u2717 ${file} is not valid JSON: ${error.message}`);
2181
2993
  return null;
2182
2994
  }
2183
- const parsed = DashboardSpecSchema.safeParse(json);
2995
+ const parsed = DashboardSpecSchema.safeParse(json2);
2184
2996
  if (!parsed.success) {
2185
2997
  console.error(`\u2717 ${file} is not a valid dashboard spec:`);
2186
2998
  for (const issue of parsed.error.issues)
@@ -2245,8 +3057,8 @@ function loadPriorEntry(dashboardPath, logFlag) {
2245
3057
  const logPath = logFlag ? resolve3(logFlag) : resolve3(dirname3(dashboardPath), "..", "public", "daily-analysis.json");
2246
3058
  if (!existsSync3(logPath)) return null;
2247
3059
  try {
2248
- const json = JSON.parse(readFileSync(logPath, "utf8"));
2249
- const entries = json.entries ?? [];
3060
+ const json2 = JSON.parse(readFileSync(logPath, "utf8"));
3061
+ const entries = json2.entries ?? [];
2250
3062
  return entries.length ? entries[entries.length - 1] : null;
2251
3063
  } catch {
2252
3064
  return null;
@@ -2331,14 +3143,14 @@ function lint(file) {
2331
3143
  console.error(`\u2717 cannot read ${file}`);
2332
3144
  return 1;
2333
3145
  }
2334
- let json;
3146
+ let json2;
2335
3147
  try {
2336
- json = JSON.parse(raw);
3148
+ json2 = JSON.parse(raw);
2337
3149
  } catch (error) {
2338
3150
  console.error(`\u2717 ${file} is not valid JSON: ${error.message}`);
2339
3151
  return 1;
2340
3152
  }
2341
- const parsed = DashboardSpecSchema.safeParse(json);
3153
+ const parsed = DashboardSpecSchema.safeParse(json2);
2342
3154
  if (!parsed.success) {
2343
3155
  console.error(`\u2717 ${file} is not a valid dashboard spec:`);
2344
3156
  for (const issue of parsed.error.issues)