zframes 0.1.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 (36) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +31 -0
  3. package/README.md +55 -0
  4. package/dist/index.js +1707 -0
  5. package/package.json +53 -0
  6. package/runtime/apple-touch-icon.png +0 -0
  7. package/runtime/assets/index-6CnH3oHt.js +719 -0
  8. package/runtime/assets/index-A5M9erCx.js +35727 -0
  9. package/runtime/assets/index-CgpsbUVX.css +1 -0
  10. package/runtime/favicon-16.png +0 -0
  11. package/runtime/favicon-32.png +0 -0
  12. package/runtime/favicon.svg +12 -0
  13. package/runtime/index.html +17 -0
  14. package/runtime/unicornStudio.legacy.umd.mjs +5745 -0
  15. package/runtime/unicornStudio.umd.mjs +6721 -0
  16. package/runtime/widget-icons/allocation.png +0 -0
  17. package/runtime/widget-icons/bitcoin-dominance.png +0 -0
  18. package/runtime/widget-icons/clock.png +0 -0
  19. package/runtime/widget-icons/daily-analysis.png +0 -0
  20. package/runtime/widget-icons/dino-game.png +0 -0
  21. package/runtime/widget-icons/fear-greed.png +0 -0
  22. package/runtime/widget-icons/funding-heatmap.png +0 -0
  23. package/runtime/widget-icons/funding-rate-chart.png +0 -0
  24. package/runtime/widget-icons/heading.png +0 -0
  25. package/runtime/widget-icons/image.png +0 -0
  26. package/runtime/widget-icons/inflation-pulse.png +0 -0
  27. package/runtime/widget-icons/market-hours.png +0 -0
  28. package/runtime/widget-icons/note.png +0 -0
  29. package/runtime/widget-icons/price-chart.png +0 -0
  30. package/runtime/widget-icons/price-compare.png +0 -0
  31. package/runtime/widget-icons/price-liveline.png +0 -0
  32. package/runtime/widget-icons/price-ticker.png +0 -0
  33. package/runtime/widget-icons/rates-board.png +0 -0
  34. package/runtime/widget-icons/top-movers.png +0 -0
  35. package/runtime/widget-icons/tvl-treemap.png +0 -0
  36. package/runtime/widget-icons/widget-icons-master.png +0 -0
package/dist/index.js ADDED
@@ -0,0 +1,1707 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync as readFileSync2 } from "fs";
5
+
6
+ // ../core/src/catalogue.ts
7
+ import { z } from "zod";
8
+ function catalogueForAI(input) {
9
+ const metas = input instanceof Map ? [...input.values()] : [...input];
10
+ return metas.map((meta) => ({
11
+ name: meta.name,
12
+ description: meta.description,
13
+ iconUrl: meta.iconUrl,
14
+ capabilities: meta.capabilities,
15
+ // io: "input" — the agent writes the *input* shape, where .default()
16
+ // fields are optional. The output shape would wrongly mark them required.
17
+ configSchema: z.toJSONSchema(meta.schema, { io: "input" })
18
+ }));
19
+ }
20
+
21
+ // ../core/src/spec.ts
22
+ import { z as z2 } from "zod";
23
+ var GridPositionSchema = z2.object({
24
+ x: z2.number().int().min(0).describe("Column offset, 0-based"),
25
+ y: z2.number().int().min(0).describe("Row offset, 0-based"),
26
+ w: z2.number().int().min(1).describe("Width in grid columns"),
27
+ h: z2.number().int().min(1).describe("Height in grid rows")
28
+ });
29
+ var FrameInstanceSchema = z2.object({
30
+ id: z2.string().describe("Unique instance id within the dashboard"),
31
+ frame: z2.string().describe("Name of a registered frame"),
32
+ title: z2.string().optional().describe(
33
+ `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
+ ),
35
+ position: GridPositionSchema,
36
+ config: z2.record(z2.string(), z2.unknown()).default({}).describe(
37
+ "Frame config; validated against the frame's own schema at render time"
38
+ )
39
+ });
40
+ var BackgroundSchema = z2.object({
41
+ type: z2.enum(["none", "gradient", "unicorn"]).default("gradient").describe(
42
+ "Background style: 'gradient' (built-in dark glow), 'unicorn' (animated Unicorn Studio scene), or 'none'."
43
+ ),
44
+ projectId: z2.string().optional().describe(
45
+ "Unicorn Studio public project id \u2014 required when type is 'unicorn'."
46
+ ),
47
+ scale: z2.number().min(0.1).max(2).default(1).describe("Unicorn scene render scale."),
48
+ dpi: z2.number().min(0.5).max(3).default(1.5).describe("Unicorn scene device-pixel-ratio cap (perf vs sharpness)."),
49
+ opacity: z2.number().min(0).max(1).default(0.16).describe(
50
+ "Opacity of the unicorn scene (0\u20131). Keep moderate (~0.15) so the animation reads as a living backdrop in the gutters without competing with the (opaque) dashboard cards."
51
+ )
52
+ }).describe("Visual background rendered behind the whole dashboard.");
53
+ var ThemeSchema = z2.object({
54
+ accentHue: z2.number().int().min(0).max(360).default(242).describe(
55
+ "Brand accent hue in degrees on the HSL color wheel (0\u2013360). Rotates the dashboard's accent \u2014 card rims, title dots, chart highlights, loading states \u2014 across the spectrum. 242 is the default zframes indigo; rough anchors: 0 red, 35 orange, 50 amber, 150 green, 190 teal, 210 blue, 280 violet, 330 pink. Semantic up/down green/red, asset logos, and per-frame chart colors stay fixed."
56
+ ),
57
+ accentSat: z2.number().min(0).max(100).default(90).describe(
58
+ "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."
59
+ )
60
+ }).describe("Dashboard-wide color identity (accent hue + saturation).");
61
+ var AppearanceSchema = z2.object({
62
+ radius: z2.number().min(0).default(18).describe(
63
+ "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."
64
+ ),
65
+ borderStrength: z2.number().min(0).max(1).default(0.22).describe(
66
+ "Opacity of each card's accent rim (0\u20131) via --zf-border-alpha. 0 = borderless flat cards, 1 = a solid outlined rim; 0.22 is the default soft edge. Hover lifts it a notch automatically."
67
+ ),
68
+ surfaceOpacity: z2.number().min(0.3).max(1).default(1).describe(
69
+ "Opacity of the card surface (0.3\u20131) via --zf-surface-opacity. 1 = opaque cards (default); lower it toward 0.5 for frosted/glassy cards that let the dashboard background show through. Kept \u22650.3 so card content stays legible."
70
+ ),
71
+ density: z2.number().min(0.6).max(1.4).default(1).describe(
72
+ "Card padding scale (0.6\u20131.4) via --zf-density. 1 = the comfortable default; <1 tightens padding for information-dense dashboards, >1 gives roomier breathing space."
73
+ ),
74
+ elevation: z2.number().min(0).max(2).default(1).describe(
75
+ "Card shadow depth (0\u20132) via --zf-elevation. 0 = flat (no drop shadow), 1 = the default resting shadow, 2 = a heavier floating lift. Scales the card's drop shadow only."
76
+ )
77
+ }).describe(
78
+ "Card surface treatment \u2014 corners, border, opacity, padding density, and shadow depth. Distinct from `grid` (geometry) and `theme` (accent color)."
79
+ );
80
+ var DashboardSpecSchema = z2.preprocess(
81
+ (raw) => {
82
+ if (!raw || typeof raw !== "object") return raw;
83
+ const r = raw;
84
+ const grid = r.grid;
85
+ if (grid && typeof grid === "object" && "radius" in grid) {
86
+ const { radius, ...restGrid } = grid;
87
+ return {
88
+ ...r,
89
+ grid: restGrid,
90
+ appearance: {
91
+ radius,
92
+ ...r.appearance ?? {}
93
+ }
94
+ };
95
+ }
96
+ return raw;
97
+ },
98
+ z2.object({
99
+ version: z2.coerce.string().default("1.0.0").describe(
100
+ 'Spec version, a semver-style string like package.json\'s ("1.0.0"). A legacy numeric `1` from an older spec is coerced to "1".'
101
+ ),
102
+ title: z2.string().describe("Dashboard name, like package.json's name."),
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.`
105
+ ),
106
+ grid: z2.object({
107
+ columns: z2.number().int().min(1).default(12),
108
+ rowHeight: z2.number().min(8).default(96),
109
+ gap: z2.number().min(0).default(12).describe(
110
+ "Pixels of space between frames (the grid gutter). 0 makes the cards flush; the editor's Layout rail exposes this as a slider."
111
+ )
112
+ }).default({ columns: 12, rowHeight: 96, gap: 12 }),
113
+ background: BackgroundSchema.default({
114
+ type: "gradient",
115
+ scale: 1,
116
+ dpi: 1.5,
117
+ opacity: 0.16
118
+ }),
119
+ theme: ThemeSchema.default({ accentHue: 242, accentSat: 90 }),
120
+ appearance: AppearanceSchema.default({
121
+ radius: 18,
122
+ borderStrength: 0.22,
123
+ surfaceOpacity: 1,
124
+ density: 1,
125
+ elevation: 1
126
+ }),
127
+ frames: z2.array(FrameInstanceSchema)
128
+ })
129
+ );
130
+
131
+ // ../core/src/frame.ts
132
+ function defineFrameMeta(meta) {
133
+ return meta;
134
+ }
135
+
136
+ // ../frames/src/schemas.ts
137
+ import { z as z3 } from "zod";
138
+ var widgetIcon = (name) => `/widget-icons/${name}.png`;
139
+ var SOURCES = {
140
+ hyperliquid: { name: "Hyperliquid", url: "https://hyperliquid.xyz" },
141
+ defillama: { name: "DeFiLlama", url: "https://defillama.com" },
142
+ coingecko: { name: "CoinGecko", url: "https://www.coingecko.com" },
143
+ alternativeMe: {
144
+ name: "alternative.me",
145
+ url: "https://alternative.me/crypto/fear-and-greed-index/"
146
+ },
147
+ bls: { name: "BLS", url: "https://www.bls.gov" },
148
+ nyFed: {
149
+ name: "NY Fed",
150
+ url: "https://www.newyorkfed.org/markets/reference-rates"
151
+ },
152
+ treasury: { name: "U.S. Treasury", url: "https://fiscaldata.treasury.gov" },
153
+ secEdgar: { name: "SEC EDGAR", url: "https://www.sec.gov/edgar" },
154
+ finra: {
155
+ name: "FINRA",
156
+ url: "https://www.finra.org/finra-data/browse-catalog/short-sale-volume-data"
157
+ }
158
+ };
159
+ var clockMeta = defineFrameMeta({
160
+ name: "clock",
161
+ iconUrl: widgetIcon("clock"),
162
+ 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.",
164
+ capabilities: [],
165
+ schema: z3.object({
166
+ timezone: z3.string().default("").describe(
167
+ `IANA timezone, e.g. "America/New_York", "Europe/London", "Asia/Tokyo", "UTC". Empty = the viewer's local timezone.`
168
+ ),
169
+ label: z3.string().default("").describe(
170
+ 'Caption under the time, e.g. "New York" or "Local". Empty hides it.'
171
+ ),
172
+ hour12: z3.boolean().default(false).describe("12-hour clock with AM/PM (true) or 24-hour (false)."),
173
+ showSeconds: z3.boolean().default(true).describe("Show seconds (HH:MM:SS) instead of just HH:MM."),
174
+ showMillis: z3.boolean().default(false).describe(
175
+ "Show milliseconds (HH:MM:SS.mmm), updated smoothly each animation frame. Implies seconds."
176
+ ),
177
+ showDate: z3.boolean().default(false).describe("Show the weekday and date under the time.")
178
+ })
179
+ });
180
+ var marketHoursMeta = defineFrameMeta({
181
+ name: "market-hours",
182
+ iconUrl: widgetIcon("market-hours"),
183
+ layout: { w: 4, h: 4, minW: 3, minH: 3 },
184
+ 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.",
185
+ capabilities: [],
186
+ schema: z3.object({
187
+ exchanges: z3.array(z3.string()).default([]).describe(
188
+ 'Exchange codes to show, e.g. ["NYSE","LSE","TSE","HKEX","SET"]. Empty = a global default set. Known codes: NYSE, NASDAQ, TSX, B3, LSE, XETRA, EURONEXT, SIX, TSE, HKEX, SSE, NSE, KRX, SGX, SET, ASX, JSE, TADAWUL.'
189
+ ),
190
+ sort: z3.enum(["region", "status", "name"]).default("region").describe(
191
+ "Order rows by world region (Americas \u2192 Europe \u2192 Asia-Pacific \u2192 Middle East/Africa), by status (open first), or alphabetically by name."
192
+ )
193
+ })
194
+ });
195
+ var fearGreedMeta = defineFrameMeta({
196
+ name: "fear-greed",
197
+ iconUrl: widgetIcon("fear-greed"),
198
+ layout: { w: 3, h: 3, minW: 2, minH: 2 },
199
+ 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.",
200
+ capabilities: ["sentiment"],
201
+ source: SOURCES.alternativeMe,
202
+ schema: z3.object({
203
+ sparklineDays: z3.number().int().min(7).max(90).default(30).describe("How many days of index history to show in the sparkline.")
204
+ })
205
+ });
206
+ var fundingRateChartMeta = defineFrameMeta({
207
+ name: "funding-rate-chart",
208
+ iconUrl: widgetIcon("funding-rate-chart"),
209
+ layout: { w: 6, h: 3, minW: 3, minH: 2 },
210
+ 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.",
211
+ capabilities: ["funding-history"],
212
+ source: SOURCES.hyperliquid,
213
+ schema: z3.object({
214
+ symbols: z3.array(z3.string()).min(1).max(6).describe(
215
+ 'Hyperliquid symbols to compare funding for, e.g. ["xyz:TSLA", "xyz:NVDA"]. Up to 6.'
216
+ ),
217
+ lookback: z3.enum(["24h", "7D", "1M"]).default("7D").describe("History window for the funding chart.")
218
+ })
219
+ });
220
+ var noteMeta = defineFrameMeta({
221
+ name: "note",
222
+ iconUrl: widgetIcon("note"),
223
+ layout: { w: 4, h: 3, minW: 2, minH: 2 },
224
+ description: "Free-form text note pinned to the dashboard \u2014 trading plans, reminders, watch levels. Needs no data provider.",
225
+ capabilities: [],
226
+ schema: z3.object({
227
+ text: z3.string().min(1).describe("The note's text content. Plain text; newlines are preserved."),
228
+ align: z3.enum(["left", "center"]).default("left").describe("Text alignment inside the card.")
229
+ })
230
+ });
231
+ var priceChartMeta = defineFrameMeta({
232
+ name: "price-chart",
233
+ iconUrl: widgetIcon("price-chart"),
234
+ layout: { w: 6, h: 3, minW: 3, minH: 2 },
235
+ 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.",
236
+ capabilities: ["ohlcv", "quote-stream"],
237
+ source: SOURCES.hyperliquid,
238
+ schema: z3.object({
239
+ symbol: z3.string().min(1).describe(
240
+ 'Hyperliquid symbol to chart. Stocks/HIP-3: "xyz:TSLA", "xyz:NVDA", "xyz:AAPL". Crypto: "BTC", "ETH".'
241
+ ),
242
+ interval: z3.enum(["1m", "5m", "15m", "1h", "4h", "1d"]).default("1h").describe("Candle interval."),
243
+ mode: z3.enum(["candle", "line"]).default("candle").describe("Candlestick or smooth line rendering."),
244
+ color: z3.string().default("#8b8df9").describe("Accent color (hex). The whole palette derives from it.")
245
+ })
246
+ });
247
+ var priceLivelineMeta = defineFrameMeta({
248
+ name: "price-liveline",
249
+ iconUrl: widgetIcon("price-liveline"),
250
+ layout: { w: 6, h: 3, minW: 4, minH: 2 },
251
+ 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.",
252
+ capabilities: ["quote-stream", "day-stats"],
253
+ source: SOURCES.hyperliquid,
254
+ schema: z3.object({
255
+ symbols: z3.array(z3.string()).min(2).max(8).describe(
256
+ 'Hyperliquid symbols to stream together, e.g. ["xyz:TSLA", "xyz:NVDA", "xyz:AAPL"] or ["BTC", "ETH", "SOL"]. 2 to 8.'
257
+ ),
258
+ windowSec: z3.number().int().min(10).max(300).default(30).describe(
259
+ "Rolling live window in seconds. 30 mirrors the zhive liveline view; use 60\u2013300 for slower dashboards."
260
+ ),
261
+ normalize: z3.boolean().default(true).describe(
262
+ "Show each asset as % movement from its first live tick (recommended when prices differ). Off = raw price overlay."
263
+ )
264
+ })
265
+ });
266
+ var priceTickerMeta = defineFrameMeta({
267
+ name: "price-ticker",
268
+ iconUrl: widgetIcon("price-ticker"),
269
+ layout: { w: 3, h: 3, minW: 2, minH: 2 },
270
+ description: "Live watchlist streaming mid prices over the Hyperliquid WebSocket with 24h change per symbol. The bread-and-butter frame for any dashboard.",
271
+ capabilities: ["quote-stream", "day-stats"],
272
+ source: SOURCES.hyperliquid,
273
+ schema: z3.object({
274
+ symbols: z3.array(z3.string()).min(1).describe(
275
+ 'Hyperliquid symbols to track, e.g. ["xyz:TSLA", "xyz:NVDA", "xyz:AAPL"]. Crypto works too: "BTC", "ETH".'
276
+ )
277
+ })
278
+ });
279
+ var topMoversMeta = defineFrameMeta({
280
+ name: "top-movers",
281
+ iconUrl: widgetIcon("top-movers"),
282
+ layout: { w: 5, h: 3, minW: 3, minH: 3 },
283
+ description: "Today's biggest stock and commodity HIP-3 gainers and losers (no bare crypto), side by side with current price and 24h change.",
284
+ capabilities: ["day-stats"],
285
+ source: SOURCES.hyperliquid,
286
+ schema: z3.object({
287
+ count: z3.number().int().min(3).max(10).default(5).describe("How many gainers and losers to list (each).")
288
+ })
289
+ });
290
+ var tvlTreemapMeta = defineFrameMeta({
291
+ name: "tvl-treemap",
292
+ iconUrl: widgetIcon("tvl-treemap"),
293
+ layout: { w: 6, h: 4, minW: 3, minH: 3 },
294
+ 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'.",
295
+ capabilities: ["tvl"],
296
+ source: SOURCES.defillama,
297
+ schema: z3.object({
298
+ topN: z3.number().int().min(3).max(30).default(12).describe("How many of the largest chains to show in the treemap.")
299
+ })
300
+ });
301
+ var bitcoinDominanceMeta = defineFrameMeta({
302
+ name: "bitcoin-dominance",
303
+ iconUrl: widgetIcon("bitcoin-dominance"),
304
+ layout: { w: 4, h: 2, minW: 3, minH: 2 },
305
+ 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.",
306
+ capabilities: ["global-market"],
307
+ source: SOURCES.coingecko,
308
+ schema: z3.object({
309
+ showTotalMarketCap: z3.boolean().default(true).describe(
310
+ "Show total crypto marketcap and its 24h change below the bar."
311
+ )
312
+ })
313
+ });
314
+ var ratesBoardMeta = defineFrameMeta({
315
+ name: "rates-board",
316
+ iconUrl: widgetIcon("rates-board"),
317
+ layout: { w: 4, h: 4, minW: 3, minH: 3 },
318
+ 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.",
319
+ capabilities: ["reference-rates", "treasury-rates"],
320
+ source: [SOURCES.nyFed, SOURCES.treasury],
321
+ schema: z3.object({
322
+ maxReferenceRates: z3.number().int().min(2).max(8).default(5).describe("How many New York Fed reference rates to show."),
323
+ showTreasuryAverageRates: z3.boolean().default(true).describe(
324
+ "Also show Treasury average interest rates by security class from Fiscal Data."
325
+ ),
326
+ maxTreasuryRates: z3.number().int().min(1).max(8).default(4).describe("How many Treasury average-rate rows to show.")
327
+ })
328
+ });
329
+ var inflationPulseMeta = defineFrameMeta({
330
+ name: "inflation-pulse",
331
+ iconUrl: widgetIcon("inflation-pulse"),
332
+ layout: { w: 4, h: 3, minW: 3, minH: 2 },
333
+ 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.",
334
+ capabilities: ["macro-series"],
335
+ source: SOURCES.bls,
336
+ schema: z3.object({
337
+ months: z3.number().int().min(13).max(36).default(18).describe("How many monthly CPI observations to show in the trend.")
338
+ })
339
+ });
340
+ var filingsFeedMeta = defineFrameMeta({
341
+ name: "filings-feed",
342
+ iconUrl: widgetIcon("filings-feed"),
343
+ layout: { w: 5, h: 4, minW: 3, minH: 3 },
344
+ 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.",
345
+ capabilities: ["filings"],
346
+ source: SOURCES.secEdgar,
347
+ schema: z3.object({
348
+ symbol: z3.string().min(1).describe(
349
+ 'Company to show filings for \u2014 a ticker ("AAPL", "NVDA"), a HIP-3 symbol ("xyz:TSLA"), or a raw SEC CIK ("320193"). Tickers outside the bundled top-500 map need a CIK.'
350
+ ),
351
+ forms: z3.enum(["important", "all", "insider"]).default("important").describe(
352
+ 'Which filings to surface: "important" = periodic & material reports (10-K, 10-Q, 8-K, S-1, proxies, 13D/G\u2026); "insider" = ownership forms (3/4/5/144); "all" = unfiltered. Always newest first.'
353
+ ),
354
+ count: z3.number().int().min(3).max(25).default(8).describe("How many filings to list (newest first).")
355
+ })
356
+ });
357
+ var yieldCurveMeta = defineFrameMeta({
358
+ name: "yield-curve",
359
+ iconUrl: widgetIcon("yield-curve"),
360
+ layout: { w: 4, h: 3, minW: 3, minH: 3 },
361
+ 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.",
362
+ capabilities: ["yield-curve"],
363
+ source: SOURCES.treasury,
364
+ schema: z3.object({
365
+ maturities: z3.array(
366
+ z3.enum([
367
+ "1M",
368
+ "2M",
369
+ "3M",
370
+ "4M",
371
+ "6M",
372
+ "1Y",
373
+ "2Y",
374
+ "3Y",
375
+ "5Y",
376
+ "7Y",
377
+ "10Y",
378
+ "20Y",
379
+ "30Y"
380
+ ])
381
+ ).min(2).max(8).default(["3M", "2Y", "5Y", "10Y", "30Y"]).describe(
382
+ "Maturities to show as labelled cells under the curve (the full curve line always shows every maturity)."
383
+ )
384
+ })
385
+ });
386
+ var fundamentalsMeta = defineFrameMeta({
387
+ name: "fundamentals",
388
+ iconUrl: widgetIcon("fundamentals"),
389
+ layout: { w: 4, h: 3, minW: 3, minH: 3 },
390
+ 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.",
391
+ capabilities: ["fundamentals"],
392
+ source: SOURCES.secEdgar,
393
+ schema: z3.object({
394
+ symbol: z3.string().min(1).describe(
395
+ 'Company to show financials for \u2014 a ticker ("AAPL", "NVDA"), a HIP-3 symbol ("xyz:NVDA"), or a raw SEC CIK ("320193"). Tickers outside the bundled top-500 map need a CIK.'
396
+ )
397
+ })
398
+ });
399
+ var shortVolumeMeta = defineFrameMeta({
400
+ name: "short-volume",
401
+ iconUrl: widgetIcon("short-volume"),
402
+ layout: { w: 5, h: 4, minW: 3, minH: 3 },
403
+ 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.",
404
+ capabilities: ["short-volume"],
405
+ source: SOURCES.finra,
406
+ schema: z3.object({
407
+ symbols: z3.array(z3.string()).min(1).max(12).describe(
408
+ 'US-listed stock tickers to show, e.g. ["TSLA","NVDA","AAPL"]. HIP-3 symbols ("xyz:TSLA") work too \u2014 the dex prefix is stripped. Crypto has no SEC/FINRA short-volume and is ignored.'
409
+ ),
410
+ sort: z3.enum(["shortPct", "volume", "symbol"]).default("shortPct").describe(
411
+ "Order rows by short % of volume (highest first), by total volume (highest first), or alphabetically by symbol."
412
+ )
413
+ })
414
+ });
415
+ var fundingHeatmapMeta = defineFrameMeta({
416
+ name: "funding-heatmap",
417
+ iconUrl: widgetIcon("funding-heatmap"),
418
+ 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.",
420
+ capabilities: ["funding-history"],
421
+ source: SOURCES.hyperliquid,
422
+ 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"].'
425
+ )
426
+ })
427
+ });
428
+ var dinoGameMeta = defineFrameMeta({
429
+ name: "dino-game",
430
+ iconUrl: widgetIcon("dino-game"),
431
+ layout: { w: 4, h: 3, minW: 3, minH: 3 },
432
+ 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.",
433
+ capabilities: [],
434
+ schema: z3.object({})
435
+ });
436
+ var imageMeta = defineFrameMeta({
437
+ name: "image",
438
+ iconUrl: widgetIcon("image"),
439
+ layout: { w: 3, h: 3, minW: 1, minH: 1 },
440
+ description: "Displays an image from a URL \u2014 logos, memes, chart screenshots, banners. Needs no data provider.",
441
+ capabilities: [],
442
+ schema: z3.object({
443
+ url: z3.string().min(1).describe("Image URL (https)."),
444
+ alt: z3.string().default("").describe("Alt text for accessibility."),
445
+ fit: z3.enum(["cover", "contain"]).default("cover").describe(
446
+ "How the image fills the frame: cover crops, contain letterboxes."
447
+ )
448
+ })
449
+ });
450
+ var headingMeta = defineFrameMeta({
451
+ name: "heading",
452
+ iconUrl: widgetIcon("heading"),
453
+ layout: { w: 12, h: 1, minW: 2, minH: 1, maxH: 1 },
454
+ 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.",
455
+ capabilities: [],
456
+ chrome: "bare",
457
+ schema: z3.object({
458
+ title: z3.string().min(1).describe("The heading text."),
459
+ subtitle: z3.string().optional().describe("Smaller supporting line under the title.")
460
+ })
461
+ });
462
+ var dailyAnalysisMeta = defineFrameMeta({
463
+ name: "daily-analysis",
464
+ iconUrl: widgetIcon("daily-analysis"),
465
+ layout: { w: 6, h: 3, minW: 3, minH: 2 },
466
+ 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.",
467
+ capabilities: [],
468
+ schema: z3.object({
469
+ src: z3.string().default("/daily-analysis.json").describe(
470
+ "URL of the analysis log the loop writes, served from the app's public/ dir. Leave as the default unless you renamed the file."
471
+ ),
472
+ entries: z3.number().int().min(1).max(5).default(1).describe(
473
+ "How many of the most recent daily entries to show (newest first)."
474
+ ),
475
+ refreshSec: z3.number().int().min(30).default(300).describe(
476
+ "How often (seconds) to re-fetch the log so a fresh brief appears without a manual reload."
477
+ )
478
+ })
479
+ });
480
+ var priceCompareMeta = defineFrameMeta({
481
+ name: "price-compare",
482
+ iconUrl: widgetIcon("price-compare"),
483
+ layout: { w: 6, h: 3, minW: 3, minH: 2 },
484
+ 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.",
485
+ capabilities: ["ohlcv"],
486
+ source: SOURCES.hyperliquid,
487
+ schema: z3.object({
488
+ symbols: z3.array(z3.string()).min(2).max(6).describe(
489
+ 'Hyperliquid symbols to overlay, e.g. ["xyz:TSLA", "xyz:NVDA", "BTC"]. 2 to 6.'
490
+ ),
491
+ lookback: z3.enum(["24h", "7D", "1M"]).default("7D").describe("History window for the comparison."),
492
+ normalize: z3.boolean().default(true).describe(
493
+ "Rebase each series to % change from the window start (recommended \u2014 lets symbols at different price levels share one axis). Off = raw price, only sensible when comparing similarly-priced symbols."
494
+ )
495
+ })
496
+ });
497
+ var allocationMeta = defineFrameMeta({
498
+ name: "allocation",
499
+ iconUrl: widgetIcon("allocation"),
500
+ layout: { w: 4, h: 4, minW: 3, minH: 3 },
501
+ 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.",
502
+ capabilities: ["quote-stream"],
503
+ source: SOURCES.hyperliquid,
504
+ schema: z3.object({
505
+ holdings: z3.array(
506
+ z3.object({
507
+ symbol: z3.string().min(1).describe('Hyperliquid symbol, e.g. "BTC", "ETH", "xyz:TSLA".'),
508
+ amount: z3.number().positive().describe(
509
+ "Units held (e.g. 0.5 BTC, 10 shares). Weights the slice by USD value = amount \xD7 live price."
510
+ )
511
+ })
512
+ ).min(2).max(8).describe("The holdings to chart. 2 to 8 positions.")
513
+ })
514
+ });
515
+ var frameMetas = [
516
+ allocationMeta,
517
+ bitcoinDominanceMeta,
518
+ clockMeta,
519
+ dailyAnalysisMeta,
520
+ dinoGameMeta,
521
+ fearGreedMeta,
522
+ filingsFeedMeta,
523
+ fundamentalsMeta,
524
+ fundingHeatmapMeta,
525
+ fundingRateChartMeta,
526
+ headingMeta,
527
+ imageMeta,
528
+ inflationPulseMeta,
529
+ marketHoursMeta,
530
+ noteMeta,
531
+ priceChartMeta,
532
+ priceCompareMeta,
533
+ priceLivelineMeta,
534
+ priceTickerMeta,
535
+ ratesBoardMeta,
536
+ shortVolumeMeta,
537
+ topMoversMeta,
538
+ tvlTreemapMeta,
539
+ yieldCurveMeta
540
+ ];
541
+
542
+ // src/init.ts
543
+ import { existsSync, mkdirSync, statSync, writeFileSync } from "fs";
544
+ import { dirname, join, resolve } from "path";
545
+ function parseArgs(args) {
546
+ let target = "dashboard.json";
547
+ let title = "my dashboard";
548
+ let author = "";
549
+ let force = false;
550
+ for (let i = 0; i < args.length; i++) {
551
+ const a = args[i];
552
+ if (a === "--title" || a === "-t") {
553
+ title = args[++i] ?? "";
554
+ } else if (a.startsWith("--title=")) {
555
+ title = a.slice("--title=".length);
556
+ } else if (a === "--author" || a === "-a") {
557
+ author = args[++i] ?? "";
558
+ } else if (a.startsWith("--author=")) {
559
+ author = a.slice("--author=".length);
560
+ } else if (a === "--force" || a === "-f") {
561
+ force = true;
562
+ } else if (!a.startsWith("-")) {
563
+ target = a;
564
+ } else {
565
+ return { error: `unknown option "${a}"` };
566
+ }
567
+ }
568
+ if (!title.trim()) return { error: "--title cannot be empty" };
569
+ return { target, title, author, force };
570
+ }
571
+ function resolveDest(target) {
572
+ const abs = resolve(process.cwd(), target);
573
+ if (abs.toLowerCase().endsWith(".json")) return abs;
574
+ return resolve(abs, "dashboard.json");
575
+ }
576
+ function skeleton(title, author) {
577
+ return {
578
+ version: "1.0.0",
579
+ title,
580
+ author,
581
+ grid: { columns: 12, rowHeight: 96, gap: 12 },
582
+ background: {
583
+ type: "unicorn",
584
+ projectId: "K42KSY4FXeXhjVOj9RgT",
585
+ opacity: 0.05
586
+ },
587
+ theme: { accentHue: 242, accentSat: 90 },
588
+ appearance: {
589
+ radius: 18,
590
+ borderStrength: 0.22,
591
+ surfaceOpacity: 1,
592
+ density: 1,
593
+ elevation: 1
594
+ },
595
+ frames: []
596
+ };
597
+ }
598
+ function init(args) {
599
+ const parsed = parseArgs(args);
600
+ if ("error" in parsed) {
601
+ console.error(`\u2717 ${parsed.error}`);
602
+ console.error(
603
+ "usage: zframes init [dir|file.json] [--title <t>] [--author <a>] [--force]"
604
+ );
605
+ return 1;
606
+ }
607
+ const dest = resolveDest(parsed.target);
608
+ if (existsSync(dest)) {
609
+ if (!parsed.force) {
610
+ console.error(`\u2717 ${dest} already exists`);
611
+ console.error(
612
+ " edit it directly, or pass --force to overwrite with a bare skeleton."
613
+ );
614
+ return 1;
615
+ }
616
+ if (!statSync(dest).isFile()) {
617
+ console.error(`\u2717 ${dest} exists but is not a file`);
618
+ return 1;
619
+ }
620
+ }
621
+ const spec = skeleton(parsed.title, parsed.author);
622
+ const check = DashboardSpecSchema.safeParse(spec);
623
+ if (!check.success) {
624
+ console.error("\u2717 internal error: bare skeleton failed validation");
625
+ for (const issue of check.error.issues)
626
+ console.error(` ${issue.path.join(".") || "(root)"}: ${issue.message}`);
627
+ return 1;
628
+ }
629
+ try {
630
+ mkdirSync(dirname(dest), { recursive: true });
631
+ writeFileSync(dest, `${JSON.stringify(spec, null, 2)}
632
+ `);
633
+ } catch (error) {
634
+ console.error(`\u2717 could not write ${dest}: ${error.message}`);
635
+ return 1;
636
+ }
637
+ const hint = parsed.target.toLowerCase().endsWith(".json") ? parsed.target : join(parsed.target, "dashboard.json");
638
+ console.log(`\u2713 wrote a bare dashboard to ${dest}`);
639
+ console.log(" next: read the catalogue, add frames, then lint + serve:");
640
+ console.log(" npx zframes@latest catalogue");
641
+ console.log(` npx zframes@latest lint ${hint}`);
642
+ console.log(` npx zframes@latest serve ${hint}`);
643
+ return 0;
644
+ }
645
+
646
+ // src/serve.ts
647
+ import { createReadStream, existsSync as existsSync2, statSync as statSync2 } from "fs";
648
+ import { createServer } from "http";
649
+ import { extname, join as join3, resolve as resolve2, sep } from "path";
650
+ import { fileURLToPath } from "url";
651
+
652
+ // ../core/src/serve.ts
653
+ import { readFile, writeFile } from "fs/promises";
654
+ var DASHBOARD_READ_ROUTE = "/__zframes/dashboard.json";
655
+ var DASHBOARD_WRITE_ROUTE = "/__zframes/dashboard";
656
+ var DASHBOARD_PROXY_ROUTE = "/__zframes/proxy";
657
+ var MAX_BODY_BYTES = 2e6;
658
+ var PROXY_ALLOW_HOSTS = /* @__PURE__ */ new Set([
659
+ "data.sec.gov",
660
+ "www.sec.gov",
661
+ "efts.sec.gov",
662
+ "www.federalreserve.gov",
663
+ "www.financialresearch.gov",
664
+ "www.nasdaqtrader.com",
665
+ "www.nyse.com",
666
+ "markets.newyorkfed.org",
667
+ "api.fiscaldata.treasury.gov",
668
+ "api.bls.gov",
669
+ "cdn.finra.org"
670
+ ]);
671
+ var PROXY_MAX_BYTES = 16e6;
672
+ var PROXY_TIMEOUT_MS = 2e4;
673
+ var PROXY_DEFAULT_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
674
+ async function handleSpecRead(absFile, res) {
675
+ res.setHeader("cache-control", "no-store");
676
+ res.setHeader("content-type", "application/json");
677
+ try {
678
+ res.statusCode = 200;
679
+ res.end(await readFile(absFile, "utf8"));
680
+ } catch (error) {
681
+ res.statusCode = error.code === "ENOENT" ? 404 : 500;
682
+ res.end(JSON.stringify({ ok: false, error: String(error) }));
683
+ }
684
+ }
685
+ function handleSpecWrite(req, res, absFile) {
686
+ if (req.method !== "PUT" && req.method !== "POST") {
687
+ res.statusCode = 405;
688
+ res.end();
689
+ return;
690
+ }
691
+ if (!String(req.headers["content-type"] ?? "").includes("application/json")) {
692
+ res.statusCode = 415;
693
+ res.end();
694
+ return;
695
+ }
696
+ let body = "";
697
+ let aborted = false;
698
+ req.on("data", (chunk) => {
699
+ if (aborted) return;
700
+ body += chunk;
701
+ if (body.length > MAX_BODY_BYTES) {
702
+ aborted = true;
703
+ res.statusCode = 413;
704
+ res.end();
705
+ req.destroy();
706
+ }
707
+ });
708
+ req.on("end", async () => {
709
+ if (aborted) return;
710
+ try {
711
+ const json = JSON.parse(body);
712
+ await writeFile(absFile, `${JSON.stringify(json, null, 2)}
713
+ `, "utf8");
714
+ res.statusCode = 200;
715
+ res.setHeader("content-type", "application/json");
716
+ res.end(JSON.stringify({ ok: true, file: absFile }));
717
+ } catch (error) {
718
+ res.statusCode = 400;
719
+ res.setHeader("content-type", "application/json");
720
+ res.end(JSON.stringify({ ok: false, error: String(error) }));
721
+ }
722
+ });
723
+ }
724
+ function proxyError(res, status, error) {
725
+ res.statusCode = status;
726
+ res.setHeader("content-type", "application/json");
727
+ res.end(JSON.stringify({ ok: false, error }));
728
+ }
729
+ async function handleProxy(req, res, opts = {}) {
730
+ if (req.method !== "GET" && req.method !== "HEAD") {
731
+ proxyError(res, 405, "proxy is GET-only");
732
+ return;
733
+ }
734
+ const query = (req.url ?? "").split("?")[1] ?? "";
735
+ const raw = new URLSearchParams(query).get("url");
736
+ let target;
737
+ try {
738
+ target = new URL(raw ?? "");
739
+ } catch {
740
+ proxyError(res, 400, "missing or invalid ?url=");
741
+ return;
742
+ }
743
+ if (target.protocol !== "https:") {
744
+ proxyError(res, 400, "only https targets are allowed");
745
+ return;
746
+ }
747
+ if (!PROXY_ALLOW_HOSTS.has(target.hostname)) {
748
+ proxyError(res, 403, `host not allowed: ${target.hostname}`);
749
+ return;
750
+ }
751
+ try {
752
+ const upstream = await fetch(target.toString(), {
753
+ headers: {
754
+ "User-Agent": opts.userAgent ?? PROXY_DEFAULT_UA,
755
+ Accept: "application/json,text/plain,*/*"
756
+ },
757
+ redirect: "follow",
758
+ signal: AbortSignal.timeout(PROXY_TIMEOUT_MS)
759
+ });
760
+ const text = await upstream.text();
761
+ if (text.length > PROXY_MAX_BYTES) {
762
+ proxyError(res, 502, "upstream response too large");
763
+ return;
764
+ }
765
+ res.statusCode = upstream.status;
766
+ res.setHeader(
767
+ "content-type",
768
+ upstream.headers.get("content-type") ?? "application/octet-stream"
769
+ );
770
+ res.setHeader("cache-control", "no-store");
771
+ res.end(text);
772
+ } catch (error) {
773
+ proxyError(res, 502, `upstream fetch failed: ${String(error)}`);
774
+ }
775
+ }
776
+
777
+ // ../core/src/agent.ts
778
+ import { spawn } from "child_process";
779
+ import { access, readFile as readFile2 } from "fs/promises";
780
+ import { constants } from "fs";
781
+ import { tmpdir } from "os";
782
+ import { dirname as dirname2, join as join2 } from "path";
783
+ var AGENTS_LIST_ROUTE = "/__zframes/agents";
784
+ var ASK_ROUTE = "/__zframes/ask";
785
+ var MAX_BODY_BYTES2 = 64e3;
786
+ var RUN_TIMEOUT_MS = 12e4;
787
+ var RUNNERS = [
788
+ {
789
+ id: "claude",
790
+ label: "Claude",
791
+ bin: "claude",
792
+ // -p/--print is non-interactive; text format prints just the answer.
793
+ buildArgs: (prompt) => ["-p", prompt, "--output-format", "text"],
794
+ readResult: async (stdout) => stdout.trim()
795
+ },
796
+ {
797
+ id: "codex",
798
+ label: "Codex",
799
+ bin: "codex",
800
+ // `exec` is non-interactive; read-only sandbox so a Q&A can't mutate
801
+ // anything, and -o writes ONLY the final message to a file (stdout carries
802
+ // session noise). --skip-git-repo-check so it runs outside a repo too.
803
+ buildArgs: (prompt, outFile) => [
804
+ "exec",
805
+ "--skip-git-repo-check",
806
+ "-s",
807
+ "read-only",
808
+ "--color",
809
+ "never",
810
+ "-o",
811
+ outFile,
812
+ prompt
813
+ ],
814
+ readResult: async (stdout, outFile) => {
815
+ try {
816
+ return (await readFile2(outFile, "utf8")).trim();
817
+ } catch {
818
+ return stdout.trim();
819
+ }
820
+ }
821
+ },
822
+ {
823
+ id: "kimi",
824
+ label: "Kimi",
825
+ bin: "kimi",
826
+ // -p/--prompt runs one prompt non-interactively and prints the response.
827
+ buildArgs: (prompt) => ["-p", prompt, "--output-format", "text"],
828
+ readResult: async (stdout) => stdout.trim()
829
+ }
830
+ ];
831
+ async function onPath(bin) {
832
+ for (const dir of (process.env.PATH ?? "").split(":")) {
833
+ if (!dir) continue;
834
+ try {
835
+ await access(join2(dir, bin), constants.X_OK);
836
+ return true;
837
+ } catch {
838
+ }
839
+ }
840
+ return false;
841
+ }
842
+ var detected = null;
843
+ function detectAgents() {
844
+ if (!detected) {
845
+ detected = Promise.all(
846
+ RUNNERS.map(async (r) => await onPath(r.bin) ? r : null)
847
+ ).then((rs) => rs.filter((r) => r !== null));
848
+ }
849
+ return detected;
850
+ }
851
+ async function buildPrompt(specFile, question) {
852
+ let title = "a live market dashboard";
853
+ const symbols = /* @__PURE__ */ new Set();
854
+ try {
855
+ const spec = JSON.parse(await readFile2(specFile, "utf8"));
856
+ if (typeof spec.title === "string" && spec.title) title = spec.title;
857
+ for (const frame of spec.frames ?? []) {
858
+ const cfg = frame.config ?? {};
859
+ if (typeof cfg.symbol === "string") symbols.add(cfg.symbol);
860
+ if (Array.isArray(cfg.symbols)) {
861
+ for (const s of cfg.symbols) if (typeof s === "string") symbols.add(s);
862
+ }
863
+ }
864
+ } catch {
865
+ }
866
+ const universe = symbols.size ? [...symbols].join(", ") : "no specific symbols";
867
+ 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.
868
+
869
+ Question: ${question}`;
870
+ }
871
+ var askCounter = 0;
872
+ function runAgent(runner, prompt, cwd) {
873
+ const outFile = join2(
874
+ tmpdir(),
875
+ `zframes-ask-${process.pid}-${++askCounter}.txt`
876
+ );
877
+ return new Promise((resolve4) => {
878
+ let child;
879
+ try {
880
+ child = spawn(runner.bin, runner.buildArgs(prompt, outFile), { cwd });
881
+ } catch (error) {
882
+ resolve4({ ok: false, error: String(error) });
883
+ return;
884
+ }
885
+ let stdout = "";
886
+ let stderr = "";
887
+ let settled = false;
888
+ const finish = (r) => {
889
+ if (settled) return;
890
+ settled = true;
891
+ clearTimeout(timer);
892
+ resolve4(r);
893
+ };
894
+ const timer = setTimeout(() => {
895
+ child.kill("SIGKILL");
896
+ finish({ ok: false, error: `${runner.label} timed out` });
897
+ }, RUN_TIMEOUT_MS);
898
+ child.stdout?.on("data", (d) => stdout += d);
899
+ child.stderr?.on("data", (d) => stderr += d);
900
+ child.on(
901
+ "error",
902
+ (e) => finish({ ok: false, error: String(e.message) })
903
+ );
904
+ child.on("close", (code) => {
905
+ if (code !== 0) {
906
+ finish({
907
+ ok: false,
908
+ error: stderr.trim() || `${runner.label} exited with code ${code}`
909
+ });
910
+ return;
911
+ }
912
+ void runner.readResult(stdout, outFile).then(
913
+ (answer) => finish(
914
+ answer ? { ok: true, answer } : { ok: false, error: `${runner.label} returned nothing` }
915
+ )
916
+ );
917
+ });
918
+ });
919
+ }
920
+ async function handleAgents(res) {
921
+ const agents = await detectAgents();
922
+ res.statusCode = 200;
923
+ res.setHeader("content-type", "application/json");
924
+ res.setHeader("cache-control", "no-store");
925
+ res.end(
926
+ JSON.stringify({ agents: agents.map(({ id, label }) => ({ id, label })) })
927
+ );
928
+ }
929
+ function handleAsk(req, res, specFile) {
930
+ if (req.method !== "POST") {
931
+ res.statusCode = 405;
932
+ res.end();
933
+ return;
934
+ }
935
+ if (!String(req.headers["content-type"] ?? "").includes("application/json")) {
936
+ res.statusCode = 415;
937
+ res.end();
938
+ return;
939
+ }
940
+ let body = "";
941
+ let aborted = false;
942
+ req.on("data", (chunk) => {
943
+ if (aborted) return;
944
+ body += chunk;
945
+ if (body.length > MAX_BODY_BYTES2) {
946
+ aborted = true;
947
+ res.statusCode = 413;
948
+ res.end();
949
+ req.destroy();
950
+ }
951
+ });
952
+ req.on("end", async () => {
953
+ if (aborted) return;
954
+ const reply = (status, payload) => {
955
+ res.statusCode = status;
956
+ res.setHeader("content-type", "application/json");
957
+ res.end(JSON.stringify(payload));
958
+ };
959
+ let question;
960
+ let requested;
961
+ try {
962
+ const parsed = JSON.parse(body);
963
+ if (typeof parsed.question !== "string" || !parsed.question.trim())
964
+ throw new Error("missing question");
965
+ question = parsed.question.trim();
966
+ requested = typeof parsed.agent === "string" ? parsed.agent : void 0;
967
+ } catch (error) {
968
+ reply(400, { ok: false, error: String(error.message) });
969
+ return;
970
+ }
971
+ const agents = await detectAgents();
972
+ if (agents.length === 0) {
973
+ reply(503, {
974
+ ok: false,
975
+ error: "no agent CLI found \u2014 install claude, codex, or kimi"
976
+ });
977
+ return;
978
+ }
979
+ const runner = agents.find((a) => a.id === requested) ?? agents[0];
980
+ const prompt = await buildPrompt(specFile, question);
981
+ const result = await runAgent(runner, prompt, dirname2(specFile));
982
+ if (result.ok)
983
+ reply(200, { ok: true, agent: runner.id, answer: result.answer });
984
+ else reply(502, { ok: false, agent: runner.id, error: result.error });
985
+ });
986
+ }
987
+
988
+ // src/serve.ts
989
+ 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
+ function parseArgs2(args) {
1008
+ let file = "dashboard.json";
1009
+ let port = DEFAULT_PORT;
1010
+ let contact = process.env.ZFRAMES_CONTACT;
1011
+ for (let i = 0; i < args.length; i++) {
1012
+ const a = args[i];
1013
+ if (a === "--port" || a === "-p") {
1014
+ port = Number(args[++i]);
1015
+ } else if (a.startsWith("--port=")) {
1016
+ port = Number(a.slice("--port=".length));
1017
+ } else if (a === "--contact") {
1018
+ contact = args[++i];
1019
+ } else if (a.startsWith("--contact=")) {
1020
+ contact = a.slice("--contact=".length);
1021
+ } else if (!a.startsWith("-")) {
1022
+ file = a;
1023
+ } else {
1024
+ return { error: `unknown option "${a}"` };
1025
+ }
1026
+ }
1027
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1028
+ return { error: "--port must be an integer between 1 and 65535" };
1029
+ }
1030
+ return { file, port, contact };
1031
+ }
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
+ function serve(args) {
1058
+ const parsed = parseArgs2(args);
1059
+ if ("error" in parsed) {
1060
+ console.error(`\u2717 ${parsed.error}`);
1061
+ console.error(
1062
+ "usage: zframes serve [dashboard.json] [--port <n>] [--contact <email>]"
1063
+ );
1064
+ return Promise.resolve(1);
1065
+ }
1066
+ const file = resolve2(process.cwd(), parsed.file);
1067
+ if (!existsSync2(file) || !statSync2(file).isFile()) {
1068
+ console.error(`\u2717 no dashboard.json at ${parsed.file}`);
1069
+ console.error(" pass a path, or run from a directory that has one.");
1070
+ return Promise.resolve(1);
1071
+ }
1072
+ const userDir = resolve2(file, "..");
1073
+ const bundleDir = fileURLToPath(new URL("../runtime", import.meta.url));
1074
+ if (!existsSync2(join3(bundleDir, "index.html"))) {
1075
+ console.error(`\u2717 runtime bundle missing at ${bundleDir}`);
1076
+ console.error(" run `pnpm build:cli` to build it.");
1077
+ return Promise.resolve(1);
1078
+ }
1079
+ return new Promise((done) => {
1080
+ const server = createServer((req, res) => {
1081
+ const rawPath = (req.url ?? "/").split("?")[0];
1082
+ let path;
1083
+ try {
1084
+ path = decodeURIComponent(rawPath);
1085
+ } catch {
1086
+ res.statusCode = 400;
1087
+ res.end();
1088
+ return;
1089
+ }
1090
+ if (path === DASHBOARD_READ_ROUTE) {
1091
+ if (req.method === "GET" || req.method === "HEAD") {
1092
+ void handleSpecRead(file, res);
1093
+ } else {
1094
+ res.statusCode = 405;
1095
+ res.end();
1096
+ }
1097
+ return;
1098
+ }
1099
+ if (path === DASHBOARD_WRITE_ROUTE) {
1100
+ handleSpecWrite(req, res, file);
1101
+ return;
1102
+ }
1103
+ if (path === AGENTS_LIST_ROUTE) {
1104
+ if (req.method === "GET") {
1105
+ void handleAgents(res);
1106
+ } else {
1107
+ res.statusCode = 405;
1108
+ res.end();
1109
+ }
1110
+ return;
1111
+ }
1112
+ if (path === ASK_ROUTE) {
1113
+ handleAsk(req, res, file);
1114
+ return;
1115
+ }
1116
+ if (path === DASHBOARD_PROXY_ROUTE) {
1117
+ void handleProxy(req, res, {
1118
+ userAgent: parsed.contact ? `zframes (${parsed.contact})` : void 0
1119
+ });
1120
+ return;
1121
+ }
1122
+ if (path.startsWith("/__zframes/")) {
1123
+ res.statusCode = 404;
1124
+ res.end();
1125
+ return;
1126
+ }
1127
+ if (req.method !== "GET" && req.method !== "HEAD") {
1128
+ res.statusCode = 405;
1129
+ res.end();
1130
+ return;
1131
+ }
1132
+ if (tryStatic(bundleDir, path, res)) return;
1133
+ if (path !== "/" && tryStatic(userDir, path, res)) return;
1134
+ sendFile(join3(bundleDir, "index.html"), res);
1135
+ });
1136
+ server.on("error", (err) => {
1137
+ if (err.code === "EADDRINUSE") {
1138
+ console.error(
1139
+ `\u2717 port ${parsed.port} is already in use \u2014 pass --port <n> or stop the other server`
1140
+ );
1141
+ } else {
1142
+ console.error(`\u2717 server error: ${err.message}`);
1143
+ }
1144
+ done(1);
1145
+ });
1146
+ server.listen(parsed.port, "127.0.0.1", () => {
1147
+ const url = `http://127.0.0.1:${parsed.port}`;
1148
+ console.log(`\u25B8 zframes serving ${parsed.file} on ${url}`);
1149
+ console.log(" live editing on \u2014 drag, resize, then Save writes back.");
1150
+ if (!parsed.contact) {
1151
+ console.log(
1152
+ " tip: pass --contact <email> so SEC/official-data requests identify you (fair-access)."
1153
+ );
1154
+ }
1155
+ });
1156
+ });
1157
+ }
1158
+
1159
+ // src/snapshot.ts
1160
+ import { existsSync as existsSync3, readFileSync } from "fs";
1161
+ import { dirname as dirname3, resolve as resolve3 } from "path";
1162
+
1163
+ // ../core/src/fetch.ts
1164
+ var DEFAULT_TIMEOUT_MS = 1e4;
1165
+ var USER_AGENT = "zframes (+https://github.com/zentryhq/zframes)";
1166
+ var PROXY_ROUTE = "/__zframes/proxy";
1167
+ async function fetchJson(url, schema, { timeoutMs = DEFAULT_TIMEOUT_MS, init: init2, proxied } = {}) {
1168
+ const headers = new Headers(init2?.headers);
1169
+ if (typeof document === "undefined" && !headers.has("User-Agent")) {
1170
+ headers.set("User-Agent", USER_AGENT);
1171
+ }
1172
+ const target = proxied && typeof document !== "undefined" ? `${PROXY_ROUTE}?url=${encodeURIComponent(url)}` : url;
1173
+ const res = await fetch(target, {
1174
+ ...init2,
1175
+ headers,
1176
+ signal: init2?.signal ?? AbortSignal.timeout(timeoutMs)
1177
+ });
1178
+ if (!res.ok) throw new Error(`${url} failed: ${res.status}`);
1179
+ const body = await res.json();
1180
+ return schema ? schema.parse(body) : body;
1181
+ }
1182
+
1183
+ // ../provider-alternativeme/src/index.ts
1184
+ var FNG_URL = "https://api.alternative.me/fng/";
1185
+ var AlternativeMeProvider = class {
1186
+ name = "alternative.me";
1187
+ capabilities = ["sentiment"];
1188
+ async getFearGreed(limit = 30) {
1189
+ const body = await fetchJson(`${FNG_URL}?limit=${limit}`);
1190
+ if (!Array.isArray(body?.data))
1191
+ throw new Error("alternative.me fng: unexpected response shape");
1192
+ return body.data.map((entry) => ({
1193
+ value: Number(entry.value),
1194
+ classification: entry.value_classification ?? "",
1195
+ time: Number(entry.timestamp) * 1e3
1196
+ })).filter(
1197
+ (point) => Number.isFinite(point.value) && Number.isFinite(point.time)
1198
+ );
1199
+ }
1200
+ };
1201
+
1202
+ // ../provider-coingecko/src/index.ts
1203
+ var GLOBAL_URL = "https://api.coingecko.com/api/v3/global";
1204
+ var CACHE_KEY = "zframes:coingecko:global";
1205
+ var TTL_MS = 12 * 6e4;
1206
+ var memo = null;
1207
+ function readCache() {
1208
+ if (memo) return memo;
1209
+ try {
1210
+ if (typeof localStorage !== "undefined") {
1211
+ const raw = localStorage.getItem(CACHE_KEY);
1212
+ if (raw) {
1213
+ memo = JSON.parse(raw);
1214
+ return memo;
1215
+ }
1216
+ }
1217
+ } catch {
1218
+ }
1219
+ return null;
1220
+ }
1221
+ function writeCache(value) {
1222
+ memo = { at: Date.now(), value };
1223
+ try {
1224
+ if (typeof localStorage !== "undefined")
1225
+ localStorage.setItem(CACHE_KEY, JSON.stringify(memo));
1226
+ } catch {
1227
+ }
1228
+ }
1229
+ var CoinGeckoProvider = class {
1230
+ name = "coingecko";
1231
+ capabilities = ["global-market"];
1232
+ async getGlobalMarket() {
1233
+ const cached = readCache();
1234
+ if (cached && Date.now() - cached.at < TTL_MS) return cached.value;
1235
+ try {
1236
+ const body = await fetchJson(GLOBAL_URL);
1237
+ if (!body?.data?.total_market_cap || !body.data.market_cap_percentage)
1238
+ throw new Error("coingecko global: unexpected response shape");
1239
+ const change = Number(body.data.market_cap_change_percentage_24h_usd);
1240
+ const value = {
1241
+ totalMarketCapUsd: body.data.total_market_cap.usd ?? 0,
1242
+ marketCapChangePct24h: Number.isFinite(change) ? change : 0,
1243
+ dominance: body.data.market_cap_percentage
1244
+ };
1245
+ writeCache(value);
1246
+ return value;
1247
+ } catch (err) {
1248
+ if (cached) return cached.value;
1249
+ throw err;
1250
+ }
1251
+ }
1252
+ };
1253
+
1254
+ // ../provider-defillama/src/index.ts
1255
+ var CHAINS_URL = "https://api.llama.fi/v2/chains";
1256
+ var DefiLlamaProvider = class {
1257
+ name = "defillama";
1258
+ capabilities = ["tvl"];
1259
+ async getTvlByChain() {
1260
+ const chains = await fetchJson(CHAINS_URL);
1261
+ if (!Array.isArray(chains))
1262
+ throw new Error("defillama chains: unexpected response shape");
1263
+ 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 }));
1264
+ }
1265
+ };
1266
+
1267
+ // ../provider-hyperliquid/src/index.ts
1268
+ var WS_URL = "wss://api.hyperliquid.xyz/ws";
1269
+ var INFO_URL = "https://api.hyperliquid.xyz/info";
1270
+ var dexOf = (symbol) => symbol.includes(":") ? symbol.split(":")[0] : "";
1271
+ async function info(body) {
1272
+ return fetchJson(INFO_URL, void 0, {
1273
+ init: {
1274
+ method: "POST",
1275
+ headers: { "Content-Type": "application/json" },
1276
+ body: JSON.stringify(body)
1277
+ }
1278
+ });
1279
+ }
1280
+ var HyperliquidProvider = class {
1281
+ name = "hyperliquid";
1282
+ capabilities = [
1283
+ "quote-stream",
1284
+ "day-stats",
1285
+ "funding-history",
1286
+ "ohlcv"
1287
+ ];
1288
+ ws = null;
1289
+ listeners = /* @__PURE__ */ new Set();
1290
+ reconnectTimer = null;
1291
+ closedByUser = false;
1292
+ /** Dexes we want an allMids subscription for ("" = default dex). */
1293
+ wantedDexes = /* @__PURE__ */ new Set([""]);
1294
+ /** Dexes actually subscribed on the current socket. */
1295
+ subscribedDexes = /* @__PURE__ */ new Set();
1296
+ /** Latest mids across all dexes, merged. */
1297
+ mergedMids = {};
1298
+ subscribeMids(onMids, symbols) {
1299
+ this.listeners.add(onMids);
1300
+ for (const symbol of symbols ?? []) this.wantedDexes.add(dexOf(symbol));
1301
+ this.ensureSocket();
1302
+ this.subscribeMissingDexes();
1303
+ return () => {
1304
+ this.listeners.delete(onMids);
1305
+ if (this.listeners.size === 0) this.teardown();
1306
+ };
1307
+ }
1308
+ async getDayStats(symbols) {
1309
+ const wholeDexes = /* @__PURE__ */ new Set();
1310
+ const concrete = /* @__PURE__ */ new Set();
1311
+ if (!symbols) {
1312
+ wholeDexes.add("");
1313
+ } else {
1314
+ for (const s of symbols) {
1315
+ if (s.endsWith(":*")) wholeDexes.add(s.slice(0, -2));
1316
+ else concrete.add(s);
1317
+ }
1318
+ }
1319
+ const dexes = new Set(wholeDexes);
1320
+ for (const s of concrete) dexes.add(dexOf(s));
1321
+ const out = {};
1322
+ await Promise.all(
1323
+ [...dexes].map(async (dex) => {
1324
+ const body = { type: "metaAndAssetCtxs" };
1325
+ if (dex) body.dex = dex;
1326
+ const [meta, ctxs] = await info(body);
1327
+ const wholeDex = wholeDexes.has(dex);
1328
+ meta.universe.forEach((asset, i) => {
1329
+ if (!wholeDex && !concrete.has(asset.name)) return;
1330
+ const ctx = ctxs[i];
1331
+ if (!ctx) return;
1332
+ const markPx = Number(ctx.markPx);
1333
+ const prevDayPx = Number(ctx.prevDayPx);
1334
+ if (!Number.isFinite(markPx) || !Number.isFinite(prevDayPx)) return;
1335
+ out[asset.name] = {
1336
+ markPx,
1337
+ prevDayPx,
1338
+ changePct: prevDayPx ? (markPx - prevDayPx) / prevDayPx * 100 : 0
1339
+ };
1340
+ });
1341
+ })
1342
+ );
1343
+ return out;
1344
+ }
1345
+ async getFundingHistory(symbols, startTimeMs) {
1346
+ const results = await Promise.all(
1347
+ symbols.map(async (coin) => {
1348
+ const entries = await info({
1349
+ type: "fundingHistory",
1350
+ coin,
1351
+ startTime: startTimeMs
1352
+ });
1353
+ const points = entries.map((entry) => ({
1354
+ time: entry.time,
1355
+ fundingRate: Number(entry.fundingRate)
1356
+ })).filter(
1357
+ (point) => Number.isFinite(point.fundingRate) && Number.isFinite(point.time)
1358
+ );
1359
+ return [coin, points];
1360
+ })
1361
+ );
1362
+ return Object.fromEntries(results);
1363
+ }
1364
+ async getCandles(symbol, interval, startTimeMs) {
1365
+ const entries = await info({
1366
+ type: "candleSnapshot",
1367
+ req: { coin: symbol, interval, startTime: startTimeMs }
1368
+ });
1369
+ return entries.map((entry) => ({
1370
+ time: entry.t,
1371
+ open: Number(entry.o),
1372
+ high: Number(entry.h),
1373
+ low: Number(entry.l),
1374
+ close: Number(entry.c),
1375
+ volume: Number(entry.v)
1376
+ }));
1377
+ }
1378
+ ensureSocket() {
1379
+ if (this.ws) return;
1380
+ this.closedByUser = false;
1381
+ this.subscribedDexes = /* @__PURE__ */ new Set();
1382
+ const ws = new WebSocket(WS_URL);
1383
+ this.ws = ws;
1384
+ ws.onopen = () => {
1385
+ this.subscribeMissingDexes();
1386
+ };
1387
+ ws.onmessage = (event) => {
1388
+ let msg;
1389
+ try {
1390
+ msg = JSON.parse(String(event.data));
1391
+ } catch {
1392
+ return;
1393
+ }
1394
+ if (msg.channel !== "allMids" || !msg.data?.mids) return;
1395
+ for (const [symbol, px] of Object.entries(msg.data.mids))
1396
+ this.mergedMids[symbol] = Number(px);
1397
+ for (const listener of this.listeners) listener(this.mergedMids);
1398
+ };
1399
+ ws.onclose = () => {
1400
+ this.ws = null;
1401
+ if (!this.closedByUser && this.listeners.size > 0) {
1402
+ this.reconnectTimer = setTimeout(() => this.ensureSocket(), 2e3);
1403
+ }
1404
+ };
1405
+ }
1406
+ subscribeMissingDexes() {
1407
+ const ws = this.ws;
1408
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
1409
+ for (const dex of this.wantedDexes) {
1410
+ if (this.subscribedDexes.has(dex)) continue;
1411
+ const subscription = { type: "allMids" };
1412
+ if (dex) subscription.dex = dex;
1413
+ ws.send(JSON.stringify({ method: "subscribe", subscription }));
1414
+ this.subscribedDexes.add(dex);
1415
+ }
1416
+ }
1417
+ teardown() {
1418
+ this.closedByUser = true;
1419
+ if (this.reconnectTimer) {
1420
+ clearTimeout(this.reconnectTimer);
1421
+ this.reconnectTimer = null;
1422
+ }
1423
+ this.ws?.close();
1424
+ this.ws = null;
1425
+ this.subscribedDexes = /* @__PURE__ */ new Set();
1426
+ this.mergedMids = {};
1427
+ }
1428
+ };
1429
+
1430
+ // src/snapshot.ts
1431
+ var DAY_MS = 864e5;
1432
+ function loadSpec(file) {
1433
+ let raw;
1434
+ try {
1435
+ raw = readFileSync(file, "utf8");
1436
+ } catch {
1437
+ console.error(`\u2717 cannot read ${file}`);
1438
+ return null;
1439
+ }
1440
+ let json;
1441
+ try {
1442
+ json = JSON.parse(raw);
1443
+ } catch (error) {
1444
+ console.error(`\u2717 ${file} is not valid JSON: ${error.message}`);
1445
+ return null;
1446
+ }
1447
+ const parsed = DashboardSpecSchema.safeParse(json);
1448
+ if (!parsed.success) {
1449
+ console.error(`\u2717 ${file} is not a valid dashboard spec:`);
1450
+ for (const issue of parsed.error.issues)
1451
+ console.error(` ${issue.path.join(".") || "(root)"}: ${issue.message}`);
1452
+ return null;
1453
+ }
1454
+ return parsed.data;
1455
+ }
1456
+ function symbolsFromSpec(spec) {
1457
+ const set = /* @__PURE__ */ new Set();
1458
+ for (const instance of spec.frames) {
1459
+ const cfg = instance.config;
1460
+ if (typeof cfg.symbol === "string") set.add(cfg.symbol);
1461
+ if (Array.isArray(cfg.symbols)) {
1462
+ for (const sym of cfg.symbols) if (typeof sym === "string") set.add(sym);
1463
+ }
1464
+ }
1465
+ return [...set];
1466
+ }
1467
+ async function safe(label, fn) {
1468
+ try {
1469
+ return await fn();
1470
+ } catch (error) {
1471
+ console.error(` \xB7 ${label} unavailable: ${error.message}`);
1472
+ return null;
1473
+ }
1474
+ }
1475
+ function rankMovers(stats, count) {
1476
+ const rows = Object.entries(stats).map(([symbol, stat]) => ({
1477
+ symbol,
1478
+ changePct: stat.changePct,
1479
+ markPx: stat.markPx
1480
+ })).filter((row) => row.markPx > 0).sort((a, b) => b.changePct - a.changePct);
1481
+ return {
1482
+ gainers: rows.slice(0, count),
1483
+ losers: rows.slice(-count).reverse()
1484
+ };
1485
+ }
1486
+ function flagValue(args, name) {
1487
+ const index = args.indexOf(name);
1488
+ return index >= 0 ? args[index + 1] : void 0;
1489
+ }
1490
+ function runMeta(args) {
1491
+ const pick = (flag, env) => flagValue(args, flag) ?? process.env[env] ?? null;
1492
+ const rawConfig = pick("--config", "ZFRAMES_CONFIG");
1493
+ let config = rawConfig;
1494
+ if (typeof rawConfig === "string") {
1495
+ try {
1496
+ config = JSON.parse(rawConfig);
1497
+ } catch {
1498
+ config = rawConfig;
1499
+ }
1500
+ }
1501
+ return {
1502
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1503
+ model: pick("--model", "ZFRAMES_MODEL"),
1504
+ effort: pick("--effort", "ZFRAMES_EFFORT"),
1505
+ config
1506
+ };
1507
+ }
1508
+ function loadPriorEntry(dashboardPath, logFlag) {
1509
+ const logPath = logFlag ? resolve3(logFlag) : resolve3(dirname3(dashboardPath), "..", "public", "daily-analysis.json");
1510
+ if (!existsSync3(logPath)) return null;
1511
+ try {
1512
+ const json = JSON.parse(readFileSync(logPath, "utf8"));
1513
+ const entries = json.entries ?? [];
1514
+ return entries.length ? entries[entries.length - 1] : null;
1515
+ } catch {
1516
+ return null;
1517
+ }
1518
+ }
1519
+ async function snapshot(args) {
1520
+ const file = args.find((arg) => !arg.startsWith("--"));
1521
+ if (!file) {
1522
+ console.error(
1523
+ "usage: zframes snapshot <dashboard.json> [--log <file>] [--date YYYY-MM-DD]\n [--model <id>] [--effort <level>] [--config <json>] (or ZFRAMES_MODEL / ZFRAMES_EFFORT / ZFRAMES_CONFIG env)"
1524
+ );
1525
+ return 1;
1526
+ }
1527
+ const logFlag = flagValue(args, "--log");
1528
+ const dateFlag = flagValue(args, "--date");
1529
+ const spec = loadSpec(file);
1530
+ if (!spec) return 1;
1531
+ const universe = symbolsFromSpec(spec);
1532
+ const featured = universe[0] ?? null;
1533
+ const hl = new HyperliquidProvider();
1534
+ const now = Date.now();
1535
+ const [userStats, marketStats, candles, funding, fearGreed, global, tvl] = await Promise.all([
1536
+ universe.length ? safe("hyperliquid day-stats", () => hl.getDayStats(universe)) : Promise.resolve({}),
1537
+ safe("hyperliquid market universe", () => hl.getDayStats()),
1538
+ featured ? safe(
1539
+ "hyperliquid candles",
1540
+ () => hl.getCandles(featured, "1d", now - 14 * DAY_MS)
1541
+ ) : Promise.resolve(null),
1542
+ universe.length ? safe(
1543
+ "hyperliquid funding",
1544
+ () => hl.getFundingHistory(universe.slice(0, 6), now - 3 * DAY_MS)
1545
+ ) : Promise.resolve({}),
1546
+ safe("fear & greed", () => new AlternativeMeProvider().getFearGreed(14)),
1547
+ safe("global market", () => new CoinGeckoProvider().getGlobalMarket()),
1548
+ safe("tvl", () => new DefiLlamaProvider().getTvlByChain())
1549
+ ]);
1550
+ const out = {
1551
+ date: dateFlag ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1552
+ run: runMeta(args),
1553
+ universe,
1554
+ featured,
1555
+ market: {
1556
+ dayStats: userStats,
1557
+ topMovers: marketStats ? rankMovers(marketStats, 5) : null,
1558
+ candles,
1559
+ funding,
1560
+ fearGreed,
1561
+ global,
1562
+ tvl: tvl ? tvl.slice(0, 12) : null
1563
+ },
1564
+ priorEntry: loadPriorEntry(file, logFlag)
1565
+ };
1566
+ console.log(JSON.stringify(out, null, 2));
1567
+ return 0;
1568
+ }
1569
+
1570
+ // src/index.ts
1571
+ var HELP = `zframes \u2014 AI-personalizable market dashboards
1572
+
1573
+ usage:
1574
+ zframes init [dir|file] write a bare, valid dashboard.json (envelope only \u2014
1575
+ version, author, grid, background, theme, empty
1576
+ frames) for the agent to fill in; --title <t>,
1577
+ --author <a>, --force to overwrite
1578
+ zframes serve [file] serve <dashboard.json> (default: ./dashboard.json)
1579
+ as a live, editable terminal at 127.0.0.1:37263
1580
+ (--port <n> to change); Save writes back to the file
1581
+ zframes catalogue print the frame catalogue as JSON Schema
1582
+ (this is what a generating agent reads)
1583
+ zframes lint <file> validate a dashboard.json; exit 1 with readable
1584
+ errors (the agent's self-correction feedback)
1585
+ zframes snapshot <file> gather a keyless market snapshot for the symbols
1586
+ on <dashboard.json> + the prior brief, as JSON on
1587
+ stdout (the deterministic half of /zframes-brief)
1588
+ zframes help this text
1589
+ `;
1590
+ function lintSpec(spec) {
1591
+ const issues = [];
1592
+ const metaByName = new Map(frameMetas.map((meta) => [meta.name, meta]));
1593
+ const seenIds = /* @__PURE__ */ new Set();
1594
+ for (const instance of spec.frames) {
1595
+ if (seenIds.has(instance.id))
1596
+ issues.push({
1597
+ frameId: instance.id,
1598
+ message: `duplicate frame id "${instance.id}"`
1599
+ });
1600
+ seenIds.add(instance.id);
1601
+ const meta = metaByName.get(instance.frame);
1602
+ if (!meta) {
1603
+ issues.push({
1604
+ frameId: instance.id,
1605
+ message: `unknown frame "${instance.frame}". available: ${[
1606
+ ...metaByName.keys()
1607
+ ].join(", ")}`
1608
+ });
1609
+ continue;
1610
+ }
1611
+ const parsed = meta.schema.safeParse(instance.config);
1612
+ if (!parsed.success) {
1613
+ for (const issue of parsed.error.issues) {
1614
+ issues.push({
1615
+ frameId: instance.id,
1616
+ message: `config.${issue.path.join(".") || "(root)"}: ${issue.message}`
1617
+ });
1618
+ }
1619
+ }
1620
+ if (instance.position.x + instance.position.w > spec.grid.columns)
1621
+ issues.push({
1622
+ frameId: instance.id,
1623
+ message: `overflows the grid: x(${instance.position.x}) + w(${instance.position.w}) > ${spec.grid.columns} columns`
1624
+ });
1625
+ }
1626
+ for (let i = 0; i < spec.frames.length; i++) {
1627
+ for (let j = i + 1; j < spec.frames.length; j++) {
1628
+ const a = spec.frames[i].position;
1629
+ const b = spec.frames[j].position;
1630
+ 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;
1631
+ if (overlap)
1632
+ issues.push({
1633
+ frameId: spec.frames[i].id,
1634
+ message: `overlaps frame "${spec.frames[j].id}"`
1635
+ });
1636
+ }
1637
+ }
1638
+ return issues;
1639
+ }
1640
+ function lint(file) {
1641
+ let raw;
1642
+ try {
1643
+ raw = readFileSync2(file, "utf8");
1644
+ } catch {
1645
+ console.error(`\u2717 cannot read ${file}`);
1646
+ return 1;
1647
+ }
1648
+ let json;
1649
+ try {
1650
+ json = JSON.parse(raw);
1651
+ } catch (error) {
1652
+ console.error(`\u2717 ${file} is not valid JSON: ${error.message}`);
1653
+ return 1;
1654
+ }
1655
+ const parsed = DashboardSpecSchema.safeParse(json);
1656
+ if (!parsed.success) {
1657
+ console.error(`\u2717 ${file} is not a valid dashboard spec:`);
1658
+ for (const issue of parsed.error.issues)
1659
+ console.error(` ${issue.path.join(".") || "(root)"}: ${issue.message}`);
1660
+ return 1;
1661
+ }
1662
+ const issues = lintSpec(parsed.data);
1663
+ if (issues.length > 0) {
1664
+ console.error(`\u2717 ${file} has ${issues.length} issue(s):`);
1665
+ for (const issue of issues)
1666
+ console.error(` [${issue.frameId ?? "spec"}] ${issue.message}`);
1667
+ return 1;
1668
+ }
1669
+ console.log(
1670
+ `\u2713 ${file} is valid \u2014 ${parsed.data.frames.length} frame(s) on a ${parsed.data.grid.columns}-column grid`
1671
+ );
1672
+ return 0;
1673
+ }
1674
+ async function main() {
1675
+ const args = process.argv.slice(2);
1676
+ const [command, arg] = args;
1677
+ switch (command) {
1678
+ case "init":
1679
+ return init(args.slice(1));
1680
+ case "catalogue":
1681
+ console.log(JSON.stringify(catalogueForAI(frameMetas), null, 2));
1682
+ return 0;
1683
+ case "lint":
1684
+ if (!arg) {
1685
+ console.error("usage: zframes lint <dashboard.json>");
1686
+ return 1;
1687
+ }
1688
+ return lint(arg);
1689
+ case "serve":
1690
+ return serve(args.slice(1));
1691
+ case "snapshot":
1692
+ return snapshot(args.slice(1));
1693
+ case "help":
1694
+ case void 0:
1695
+ console.log(HELP);
1696
+ return 0;
1697
+ default:
1698
+ console.error(`unknown command "${command}"
1699
+ `);
1700
+ console.log(HELP);
1701
+ return 1;
1702
+ }
1703
+ }
1704
+ main().then((code) => process.exit(code));
1705
+ export {
1706
+ lintSpec
1707
+ };