zframes 0.1.1 → 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.
- package/NOTICE +2 -0
- package/dist/index.js +1727 -217
- package/package.json +7 -6
- package/runtime/assets/d3-B3esOK0k.js +1 -0
- package/runtime/assets/editor-DVTcqJo4.js +68 -0
- package/runtime/assets/editor-DnV0I9s-.css +1 -0
- package/runtime/assets/gridstack-BxxvPIFU.js +9 -0
- package/runtime/assets/gridstack-FIb_WQSt.css +1 -0
- package/runtime/assets/index-DEGnog6Q.css +1 -0
- package/runtime/assets/{index-A5M9erCx.js → index-DPlG_EDx.js} +1 -1
- package/runtime/assets/index-KRy1feFG.js +761 -0
- package/runtime/assets/liveline-C5zt2WuS.js +1 -0
- package/runtime/assets/react-qoZPGuNy.js +49 -0
- package/runtime/index.html +6 -3
- package/runtime/widget-icons/breakeven.png +0 -0
- package/runtime/widget-icons/breathing.png +0 -0
- package/runtime/widget-icons/btc-blocks.png +0 -0
- package/runtime/widget-icons/btc-difficulty.png +0 -0
- package/runtime/widget-icons/btc-fees.png +0 -0
- package/runtime/widget-icons/btc-hashrate.png +0 -0
- package/runtime/widget-icons/btc-mempool.png +0 -0
- package/runtime/widget-icons/calculator.png +0 -0
- package/runtime/widget-icons/checklist.png +0 -0
- package/runtime/widget-icons/coin-movers.png +0 -0
- package/runtime/widget-icons/countdown.png +0 -0
- package/runtime/widget-icons/day-meter.png +0 -0
- package/runtime/widget-icons/dex-volume-chart.png +0 -0
- package/runtime/widget-icons/dex-volume-treemap.png +0 -0
- package/runtime/widget-icons/dice.png +0 -0
- package/runtime/widget-icons/divider.png +0 -0
- package/runtime/widget-icons/drawdy.png +0 -0
- package/runtime/widget-icons/filings-feed.png +0 -0
- package/runtime/widget-icons/financial-stress.png +0 -0
- package/runtime/widget-icons/flappy-bird.png +0 -0
- package/runtime/widget-icons/fundamentals.png +0 -0
- package/runtime/widget-icons/holiday-calendar.png +0 -0
- package/runtime/widget-icons/journal-log.png +0 -0
- package/runtime/widget-icons/journal-open.png +0 -0
- package/runtime/widget-icons/journal-results.png +0 -0
- package/runtime/widget-icons/journal-score.png +0 -0
- package/runtime/widget-icons/labor-market.png +0 -0
- package/runtime/widget-icons/lightning-stats.png +0 -0
- package/runtime/widget-icons/link-grid.png +0 -0
- package/runtime/widget-icons/market-cap-treemap.png +0 -0
- package/runtime/widget-icons/marquee.png +0 -0
- package/runtime/widget-icons/mining-pools.png +0 -0
- package/runtime/widget-icons/national-debt.png +0 -0
- package/runtime/widget-icons/news-feed.png +0 -0
- package/runtime/widget-icons/open-interest.png +0 -0
- package/runtime/widget-icons/options-iv.png +0 -0
- package/runtime/widget-icons/options-oi-strike.png +0 -0
- package/runtime/widget-icons/options-put-call.png +0 -0
- package/runtime/widget-icons/pomodoro.png +0 -0
- package/runtime/widget-icons/portfolio-allocation.png +0 -0
- package/runtime/widget-icons/portfolio-holdings.png +0 -0
- package/runtime/widget-icons/portfolio-value.png +0 -0
- package/runtime/widget-icons/protocol-fees-treemap.png +0 -0
- package/runtime/widget-icons/protocol-tvl-chart.png +0 -0
- package/runtime/widget-icons/protocol-tvl-treemap.png +0 -0
- package/runtime/widget-icons/quote.png +0 -0
- package/runtime/widget-icons/returns-projector.png +0 -0
- package/runtime/widget-icons/risk-reward.png +0 -0
- package/runtime/widget-icons/rules-card.png +0 -0
- package/runtime/widget-icons/session-progress.png +0 -0
- package/runtime/widget-icons/short-volume.png +0 -0
- package/runtime/widget-icons/snake.png +0 -0
- package/runtime/widget-icons/spotify-embed.png +0 -0
- package/runtime/widget-icons/stopwatch.png +0 -0
- package/runtime/widget-icons/treasury-auctions.png +0 -0
- package/runtime/widget-icons/video.png +0 -0
- package/runtime/widget-icons/yield-curve.png +0 -0
- package/runtime/assets/index-6CnH3oHt.js +0 -719
- package/runtime/assets/index-CgpsbUVX.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."
|
|
59
99
|
)
|
|
60
|
-
}).describe(
|
|
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."
|
|
112
|
+
)
|
|
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."
|
|
@@ -101,22 +156,50 @@ var DashboardSpecSchema = z2.preprocess(
|
|
|
101
156
|
),
|
|
102
157
|
title: z2.string().describe("Dashboard name, like package.json's name."),
|
|
103
158
|
author: z2.string().optional().describe(
|
|
104
|
-
`Who made this dashboard \u2014 a free-form credit, like package.json's author ("
|
|
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
|
-
|
|
108
|
-
|
|
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({
|
|
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({
|
|
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,
|
|
@@ -154,13 +237,21 @@ var SOURCES = {
|
|
|
154
237
|
finra: {
|
|
155
238
|
name: "FINRA",
|
|
156
239
|
url: "https://www.finra.org/finra-data/browse-catalog/short-sale-volume-data"
|
|
157
|
-
}
|
|
240
|
+
},
|
|
241
|
+
ofr: {
|
|
242
|
+
name: "OFR",
|
|
243
|
+
url: "https://www.financialresearch.gov/financial-stress-index/"
|
|
244
|
+
},
|
|
245
|
+
mempool: { name: "mempool.space", url: "https://mempool.space" },
|
|
246
|
+
deribit: { name: "Deribit", url: "https://www.deribit.com" },
|
|
247
|
+
coinpaprika: { name: "Coinpaprika", url: "https://coinpaprika.com" }
|
|
158
248
|
};
|
|
159
249
|
var clockMeta = defineFrameMeta({
|
|
160
250
|
name: "clock",
|
|
251
|
+
category: "tools",
|
|
161
252
|
iconUrl: widgetIcon("clock"),
|
|
162
253
|
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.",
|
|
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.",
|
|
164
255
|
capabilities: [],
|
|
165
256
|
schema: z3.object({
|
|
166
257
|
timezone: z3.string().default("").describe(
|
|
@@ -174,11 +265,15 @@ var clockMeta = defineFrameMeta({
|
|
|
174
265
|
showMillis: z3.boolean().default(false).describe(
|
|
175
266
|
"Show milliseconds (HH:MM:SS.mmm), updated smoothly each animation frame. Implies seconds."
|
|
176
267
|
),
|
|
177
|
-
showDate: z3.boolean().default(false).describe("Show the weekday and date under the time.")
|
|
268
|
+
showDate: z3.boolean().default(false).describe("Show the weekday and date under the time."),
|
|
269
|
+
showTimezone: z3.boolean().default(true).describe(
|
|
270
|
+
'Show the timezone abbreviation (e.g. "EST", "GMT+7", "UTC") in the caption. Combines with the label when set, e.g. "New York \xB7 EST".'
|
|
271
|
+
)
|
|
178
272
|
})
|
|
179
273
|
});
|
|
180
274
|
var marketHoursMeta = defineFrameMeta({
|
|
181
275
|
name: "market-hours",
|
|
276
|
+
category: "tools",
|
|
182
277
|
iconUrl: widgetIcon("market-hours"),
|
|
183
278
|
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
184
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.",
|
|
@@ -194,6 +289,7 @@ var marketHoursMeta = defineFrameMeta({
|
|
|
194
289
|
});
|
|
195
290
|
var fearGreedMeta = defineFrameMeta({
|
|
196
291
|
name: "fear-greed",
|
|
292
|
+
category: "sentiment",
|
|
197
293
|
iconUrl: widgetIcon("fear-greed"),
|
|
198
294
|
layout: { w: 3, h: 3, minW: 2, minH: 2 },
|
|
199
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.",
|
|
@@ -205,6 +301,7 @@ var fearGreedMeta = defineFrameMeta({
|
|
|
205
301
|
});
|
|
206
302
|
var fundingRateChartMeta = defineFrameMeta({
|
|
207
303
|
name: "funding-rate-chart",
|
|
304
|
+
category: "derivatives",
|
|
208
305
|
iconUrl: widgetIcon("funding-rate-chart"),
|
|
209
306
|
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
210
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.",
|
|
@@ -219,6 +316,7 @@ var fundingRateChartMeta = defineFrameMeta({
|
|
|
219
316
|
});
|
|
220
317
|
var noteMeta = defineFrameMeta({
|
|
221
318
|
name: "note",
|
|
319
|
+
category: "layout",
|
|
222
320
|
iconUrl: widgetIcon("note"),
|
|
223
321
|
layout: { w: 4, h: 3, minW: 2, minH: 2 },
|
|
224
322
|
description: "Free-form text note pinned to the dashboard \u2014 trading plans, reminders, watch levels. Needs no data provider.",
|
|
@@ -230,6 +328,7 @@ var noteMeta = defineFrameMeta({
|
|
|
230
328
|
});
|
|
231
329
|
var priceChartMeta = defineFrameMeta({
|
|
232
330
|
name: "price-chart",
|
|
331
|
+
category: "markets",
|
|
233
332
|
iconUrl: widgetIcon("price-chart"),
|
|
234
333
|
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
235
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.",
|
|
@@ -246,6 +345,7 @@ var priceChartMeta = defineFrameMeta({
|
|
|
246
345
|
});
|
|
247
346
|
var priceLivelineMeta = defineFrameMeta({
|
|
248
347
|
name: "price-liveline",
|
|
348
|
+
category: "markets",
|
|
249
349
|
iconUrl: widgetIcon("price-liveline"),
|
|
250
350
|
layout: { w: 6, h: 3, minW: 4, minH: 2 },
|
|
251
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.",
|
|
@@ -265,6 +365,7 @@ var priceLivelineMeta = defineFrameMeta({
|
|
|
265
365
|
});
|
|
266
366
|
var priceTickerMeta = defineFrameMeta({
|
|
267
367
|
name: "price-ticker",
|
|
368
|
+
category: "markets",
|
|
268
369
|
iconUrl: widgetIcon("price-ticker"),
|
|
269
370
|
layout: { w: 3, h: 3, minW: 2, minH: 2 },
|
|
270
371
|
description: "Live watchlist streaming mid prices over the Hyperliquid WebSocket with 24h change per symbol. The bread-and-butter frame for any dashboard.",
|
|
@@ -278,6 +379,7 @@ var priceTickerMeta = defineFrameMeta({
|
|
|
278
379
|
});
|
|
279
380
|
var topMoversMeta = defineFrameMeta({
|
|
280
381
|
name: "top-movers",
|
|
382
|
+
category: "markets",
|
|
281
383
|
iconUrl: widgetIcon("top-movers"),
|
|
282
384
|
layout: { w: 5, h: 3, minW: 3, minH: 3 },
|
|
283
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.",
|
|
@@ -289,6 +391,7 @@ var topMoversMeta = defineFrameMeta({
|
|
|
289
391
|
});
|
|
290
392
|
var tvlTreemapMeta = defineFrameMeta({
|
|
291
393
|
name: "tvl-treemap",
|
|
394
|
+
category: "crypto",
|
|
292
395
|
iconUrl: widgetIcon("tvl-treemap"),
|
|
293
396
|
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
294
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'.",
|
|
@@ -300,6 +403,7 @@ var tvlTreemapMeta = defineFrameMeta({
|
|
|
300
403
|
});
|
|
301
404
|
var bitcoinDominanceMeta = defineFrameMeta({
|
|
302
405
|
name: "bitcoin-dominance",
|
|
406
|
+
category: "crypto",
|
|
303
407
|
iconUrl: widgetIcon("bitcoin-dominance"),
|
|
304
408
|
layout: { w: 4, h: 2, minW: 3, minH: 2 },
|
|
305
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.",
|
|
@@ -313,6 +417,7 @@ var bitcoinDominanceMeta = defineFrameMeta({
|
|
|
313
417
|
});
|
|
314
418
|
var ratesBoardMeta = defineFrameMeta({
|
|
315
419
|
name: "rates-board",
|
|
420
|
+
category: "macro",
|
|
316
421
|
iconUrl: widgetIcon("rates-board"),
|
|
317
422
|
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
318
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.",
|
|
@@ -328,6 +433,7 @@ var ratesBoardMeta = defineFrameMeta({
|
|
|
328
433
|
});
|
|
329
434
|
var inflationPulseMeta = defineFrameMeta({
|
|
330
435
|
name: "inflation-pulse",
|
|
436
|
+
category: "macro",
|
|
331
437
|
iconUrl: widgetIcon("inflation-pulse"),
|
|
332
438
|
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
333
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.",
|
|
@@ -337,8 +443,65 @@ var inflationPulseMeta = defineFrameMeta({
|
|
|
337
443
|
months: z3.number().int().min(13).max(36).default(18).describe("How many monthly CPI observations to show in the trend.")
|
|
338
444
|
})
|
|
339
445
|
});
|
|
446
|
+
var financialStressMeta = defineFrameMeta({
|
|
447
|
+
name: "financial-stress",
|
|
448
|
+
category: "macro",
|
|
449
|
+
iconUrl: widgetIcon("financial-stress"),
|
|
450
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
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.",
|
|
452
|
+
capabilities: ["financial-stress"],
|
|
453
|
+
source: SOURCES.ofr,
|
|
454
|
+
schema: z3.object({
|
|
455
|
+
trendDays: z3.number().int().min(20).max(90).default(60).describe("How many recent daily readings to plot in the trend line."),
|
|
456
|
+
showCategories: z3.boolean().default(true).describe(
|
|
457
|
+
"Show the five category contributions (credit, equity valuation, safe assets, funding, volatility) under the headline."
|
|
458
|
+
)
|
|
459
|
+
})
|
|
460
|
+
});
|
|
461
|
+
var nationalDebtMeta = defineFrameMeta({
|
|
462
|
+
name: "national-debt",
|
|
463
|
+
category: "macro",
|
|
464
|
+
iconUrl: widgetIcon("national-debt"),
|
|
465
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
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.",
|
|
467
|
+
capabilities: ["national-debt"],
|
|
468
|
+
source: SOURCES.treasury,
|
|
469
|
+
schema: z3.object({
|
|
470
|
+
trendDays: z3.number().int().min(30).max(365).default(180).describe(
|
|
471
|
+
"How many business days of history to load for the trend and the change figure."
|
|
472
|
+
),
|
|
473
|
+
showSplit: z3.boolean().default(true).describe(
|
|
474
|
+
"Show the debt-held-by-the-public vs intragovernmental-holdings split."
|
|
475
|
+
)
|
|
476
|
+
})
|
|
477
|
+
});
|
|
478
|
+
var laborMarketMeta = defineFrameMeta({
|
|
479
|
+
name: "labor-market",
|
|
480
|
+
category: "macro",
|
|
481
|
+
iconUrl: widgetIcon("labor-market"),
|
|
482
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
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.",
|
|
484
|
+
capabilities: ["macro-series"],
|
|
485
|
+
source: SOURCES.bls,
|
|
486
|
+
schema: z3.object({
|
|
487
|
+
months: z3.number().int().min(13).max(36).default(18).describe("How many monthly observations to show in the trend.")
|
|
488
|
+
})
|
|
489
|
+
});
|
|
490
|
+
var treasuryAuctionsMeta = defineFrameMeta({
|
|
491
|
+
name: "treasury-auctions",
|
|
492
|
+
category: "macro",
|
|
493
|
+
iconUrl: widgetIcon("treasury-auctions"),
|
|
494
|
+
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
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.",
|
|
496
|
+
capabilities: ["treasury-auctions"],
|
|
497
|
+
source: SOURCES.treasury,
|
|
498
|
+
schema: z3.object({
|
|
499
|
+
count: z3.number().int().min(3).max(20).default(8).describe("How many recent auctions to list (newest first).")
|
|
500
|
+
})
|
|
501
|
+
});
|
|
340
502
|
var filingsFeedMeta = defineFrameMeta({
|
|
341
503
|
name: "filings-feed",
|
|
504
|
+
category: "equities",
|
|
342
505
|
iconUrl: widgetIcon("filings-feed"),
|
|
343
506
|
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
344
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.",
|
|
@@ -356,6 +519,7 @@ var filingsFeedMeta = defineFrameMeta({
|
|
|
356
519
|
});
|
|
357
520
|
var yieldCurveMeta = defineFrameMeta({
|
|
358
521
|
name: "yield-curve",
|
|
522
|
+
category: "macro",
|
|
359
523
|
iconUrl: widgetIcon("yield-curve"),
|
|
360
524
|
layout: { w: 4, h: 3, minW: 3, minH: 3 },
|
|
361
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.",
|
|
@@ -385,6 +549,7 @@ var yieldCurveMeta = defineFrameMeta({
|
|
|
385
549
|
});
|
|
386
550
|
var fundamentalsMeta = defineFrameMeta({
|
|
387
551
|
name: "fundamentals",
|
|
552
|
+
category: "equities",
|
|
388
553
|
iconUrl: widgetIcon("fundamentals"),
|
|
389
554
|
layout: { w: 4, h: 3, minW: 3, minH: 3 },
|
|
390
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.",
|
|
@@ -398,6 +563,7 @@ var fundamentalsMeta = defineFrameMeta({
|
|
|
398
563
|
});
|
|
399
564
|
var shortVolumeMeta = defineFrameMeta({
|
|
400
565
|
name: "short-volume",
|
|
566
|
+
category: "equities",
|
|
401
567
|
iconUrl: widgetIcon("short-volume"),
|
|
402
568
|
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
403
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.",
|
|
@@ -414,19 +580,21 @@ var shortVolumeMeta = defineFrameMeta({
|
|
|
414
580
|
});
|
|
415
581
|
var fundingHeatmapMeta = defineFrameMeta({
|
|
416
582
|
name: "funding-heatmap",
|
|
583
|
+
category: "derivatives",
|
|
417
584
|
iconUrl: widgetIcon("funding-heatmap"),
|
|
418
585
|
layout: { w: 6, h: 3, minW: 4, minH: 3 },
|
|
419
|
-
description: "Heatmap of perp funding rates \u2014 symbols as rows,
|
|
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.",
|
|
420
587
|
capabilities: ["funding-history"],
|
|
421
588
|
source: SOURCES.hyperliquid,
|
|
422
589
|
schema: z3.object({
|
|
423
|
-
symbols: z3.array(z3.string()).min(
|
|
424
|
-
'Hyperliquid symbols as heatmap rows, e.g. ["xyz:TSLA", "xyz:NVDA", "xyz:AAPL"].'
|
|
590
|
+
symbols: z3.array(z3.string()).min(2).max(8).describe(
|
|
591
|
+
'Hyperliquid symbols as heatmap rows \u2014 at least 2, since the heatmap compares funding across symbols, e.g. ["xyz:TSLA", "xyz:NVDA", "xyz:AAPL"].'
|
|
425
592
|
)
|
|
426
593
|
})
|
|
427
594
|
});
|
|
428
595
|
var dinoGameMeta = defineFrameMeta({
|
|
429
596
|
name: "dino-game",
|
|
597
|
+
category: "games",
|
|
430
598
|
iconUrl: widgetIcon("dino-game"),
|
|
431
599
|
layout: { w: 4, h: 3, minW: 3, minH: 3 },
|
|
432
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.",
|
|
@@ -435,6 +603,7 @@ var dinoGameMeta = defineFrameMeta({
|
|
|
435
603
|
});
|
|
436
604
|
var imageMeta = defineFrameMeta({
|
|
437
605
|
name: "image",
|
|
606
|
+
category: "layout",
|
|
438
607
|
iconUrl: widgetIcon("image"),
|
|
439
608
|
layout: { w: 3, h: 3, minW: 1, minH: 1 },
|
|
440
609
|
description: "Displays an image from a URL \u2014 logos, memes, chart screenshots, banners. Needs no data provider.",
|
|
@@ -449,6 +618,7 @@ var imageMeta = defineFrameMeta({
|
|
|
449
618
|
});
|
|
450
619
|
var headingMeta = defineFrameMeta({
|
|
451
620
|
name: "heading",
|
|
621
|
+
category: "layout",
|
|
452
622
|
iconUrl: widgetIcon("heading"),
|
|
453
623
|
layout: { w: 12, h: 1, minW: 2, minH: 1, maxH: 1 },
|
|
454
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.",
|
|
@@ -461,6 +631,7 @@ var headingMeta = defineFrameMeta({
|
|
|
461
631
|
});
|
|
462
632
|
var dailyAnalysisMeta = defineFrameMeta({
|
|
463
633
|
name: "daily-analysis",
|
|
634
|
+
category: "tools",
|
|
464
635
|
iconUrl: widgetIcon("daily-analysis"),
|
|
465
636
|
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
466
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.",
|
|
@@ -477,8 +648,51 @@ var dailyAnalysisMeta = defineFrameMeta({
|
|
|
477
648
|
)
|
|
478
649
|
})
|
|
479
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
|
+
});
|
|
480
693
|
var priceCompareMeta = defineFrameMeta({
|
|
481
694
|
name: "price-compare",
|
|
695
|
+
category: "markets",
|
|
482
696
|
iconUrl: widgetIcon("price-compare"),
|
|
483
697
|
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
484
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.",
|
|
@@ -494,26 +708,693 @@ var priceCompareMeta = defineFrameMeta({
|
|
|
494
708
|
)
|
|
495
709
|
})
|
|
496
710
|
});
|
|
497
|
-
var
|
|
498
|
-
|
|
499
|
-
|
|
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,
|
|
727
|
+
schema: z3.object({
|
|
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
|
+
)
|
|
732
|
+
})
|
|
733
|
+
});
|
|
734
|
+
var portfolioAllocationMeta = defineFrameMeta({
|
|
735
|
+
name: "portfolio-allocation",
|
|
736
|
+
category: "portfolio",
|
|
737
|
+
iconUrl: widgetIcon("portfolio-allocation"),
|
|
500
738
|
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
501
|
-
description: "Donut of
|
|
502
|
-
capabilities: ["quote-stream"],
|
|
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
|
+
});
|
|
754
|
+
var newsFeedMeta = defineFrameMeta({
|
|
755
|
+
name: "news-feed",
|
|
756
|
+
category: "sentiment",
|
|
757
|
+
iconUrl: widgetIcon("news-feed"),
|
|
758
|
+
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
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.',
|
|
760
|
+
capabilities: ["news"],
|
|
761
|
+
schema: z3.object({
|
|
762
|
+
source: z3.enum([
|
|
763
|
+
"coindesk",
|
|
764
|
+
"cointelegraph",
|
|
765
|
+
"decrypt",
|
|
766
|
+
"cnbc",
|
|
767
|
+
"nasdaq",
|
|
768
|
+
"stocks"
|
|
769
|
+
]).default("coindesk").describe(
|
|
770
|
+
'Which feed to show. Crypto press: "coindesk", "cointelegraph", "decrypt". Markets/macro: "cnbc", "nasdaq". "stocks" = per-company headlines (Google News) for the tickers in `symbols`.'
|
|
771
|
+
),
|
|
772
|
+
symbols: z3.array(z3.string()).default([]).describe(
|
|
773
|
+
'Only used when source is "stocks": stock tickers to pull headlines for, e.g. ["TSLA","NVDA","AAPL"]. HIP-3 symbols ("xyz:TSLA") work too \u2014 the dex prefix is stripped. Ignored for the other sources.'
|
|
774
|
+
),
|
|
775
|
+
count: z3.number().int().min(3).max(20).default(8).describe("How many headlines to list (newest first).")
|
|
776
|
+
})
|
|
777
|
+
});
|
|
778
|
+
var dexVolumeTreemapMeta = defineFrameMeta({
|
|
779
|
+
name: "dex-volume-treemap",
|
|
780
|
+
category: "crypto",
|
|
781
|
+
iconUrl: widgetIcon("dex-volume-treemap"),
|
|
782
|
+
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
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.",
|
|
784
|
+
capabilities: ["dex-volume"],
|
|
785
|
+
source: SOURCES.defillama,
|
|
786
|
+
schema: z3.object({
|
|
787
|
+
topN: z3.number().int().min(3).max(30).default(12).describe("How many of the highest-volume DEX protocols to show.")
|
|
788
|
+
})
|
|
789
|
+
});
|
|
790
|
+
var dexVolumeChartMeta = defineFrameMeta({
|
|
791
|
+
name: "dex-volume-chart",
|
|
792
|
+
category: "crypto",
|
|
793
|
+
iconUrl: widgetIcon("dex-volume-chart"),
|
|
794
|
+
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
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).",
|
|
796
|
+
capabilities: ["dex-volume"],
|
|
797
|
+
source: SOURCES.defillama,
|
|
798
|
+
schema: z3.object({
|
|
799
|
+
protocols: z3.array(z3.string()).min(1).max(6).describe(
|
|
800
|
+
'DeFiLlama DEX protocol slugs (lowercase, hyphenated), e.g. ["uniswap", "pancakeswap", "aerodrome-slipstream"]. 1 to 6.'
|
|
801
|
+
),
|
|
802
|
+
lookback: z3.enum(["7D", "1M", "3M"]).default("1M").describe("History window for the chart.")
|
|
803
|
+
})
|
|
804
|
+
});
|
|
805
|
+
var protocolTvlTreemapMeta = defineFrameMeta({
|
|
806
|
+
name: "protocol-tvl-treemap",
|
|
807
|
+
category: "crypto",
|
|
808
|
+
iconUrl: widgetIcon("protocol-tvl-treemap"),
|
|
809
|
+
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
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).",
|
|
811
|
+
capabilities: ["protocol-tvl"],
|
|
812
|
+
source: SOURCES.defillama,
|
|
813
|
+
schema: z3.object({
|
|
814
|
+
topN: z3.number().int().min(3).max(30).default(12).describe("How many of the largest protocols by TVL to show.")
|
|
815
|
+
})
|
|
816
|
+
});
|
|
817
|
+
var protocolTvlChartMeta = defineFrameMeta({
|
|
818
|
+
name: "protocol-tvl-chart",
|
|
819
|
+
category: "crypto",
|
|
820
|
+
iconUrl: widgetIcon("protocol-tvl-chart"),
|
|
821
|
+
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
822
|
+
description: "Multi-series line chart of total value locked (TVL) for several DeFi protocols over a lookback window. Data from DeFiLlama (daily granularity).",
|
|
823
|
+
capabilities: ["protocol-tvl"],
|
|
824
|
+
source: SOURCES.defillama,
|
|
825
|
+
schema: z3.object({
|
|
826
|
+
protocols: z3.array(z3.string()).min(1).max(6).describe(
|
|
827
|
+
'DeFiLlama protocol slugs (lowercase, hyphenated), e.g. ["lido", "aave", "eigenlayer"]. 1 to 6.'
|
|
828
|
+
),
|
|
829
|
+
lookback: z3.enum(["7D", "1M", "3M"]).default("1M").describe("History window for the chart.")
|
|
830
|
+
})
|
|
831
|
+
});
|
|
832
|
+
var protocolFeesTreemapMeta = defineFrameMeta({
|
|
833
|
+
name: "protocol-fees-treemap",
|
|
834
|
+
category: "crypto",
|
|
835
|
+
iconUrl: widgetIcon("protocol-fees-treemap"),
|
|
836
|
+
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
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.",
|
|
838
|
+
capabilities: ["protocol-fees"],
|
|
839
|
+
source: SOURCES.defillama,
|
|
840
|
+
schema: z3.object({
|
|
841
|
+
topN: z3.number().int().min(3).max(30).default(12).describe("How many of the highest fee-earning protocols to show.")
|
|
842
|
+
})
|
|
843
|
+
});
|
|
844
|
+
var marketCapTreemapMeta = defineFrameMeta({
|
|
845
|
+
name: "market-cap-treemap",
|
|
846
|
+
category: "crypto",
|
|
847
|
+
iconUrl: widgetIcon("market-cap-treemap"),
|
|
848
|
+
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
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.",
|
|
850
|
+
capabilities: ["coin-markets"],
|
|
851
|
+
source: SOURCES.coingecko,
|
|
852
|
+
schema: z3.object({
|
|
853
|
+
topN: z3.number().int().min(5).max(50).default(12).describe(
|
|
854
|
+
"How many of the largest coins by market cap to show (up to 50)."
|
|
855
|
+
)
|
|
856
|
+
})
|
|
857
|
+
});
|
|
858
|
+
var openInterestMeta = defineFrameMeta({
|
|
859
|
+
name: "open-interest",
|
|
860
|
+
category: "derivatives",
|
|
861
|
+
iconUrl: widgetIcon("open-interest"),
|
|
862
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
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.',
|
|
864
|
+
capabilities: ["open-interest"],
|
|
503
865
|
source: SOURCES.hyperliquid,
|
|
504
866
|
schema: z3.object({
|
|
505
|
-
|
|
867
|
+
symbols: z3.array(z3.string()).min(1).max(20).describe(
|
|
868
|
+
`Hyperliquid symbols to compare open interest for, e.g. ["BTC", "ETH", "xyz:TSLA"]. A "<dex>:*" wildcard (e.g. "xyz:*") pulls that dex's entire universe.`
|
|
869
|
+
)
|
|
870
|
+
})
|
|
871
|
+
});
|
|
872
|
+
var snakeMeta = defineFrameMeta({
|
|
873
|
+
name: "snake",
|
|
874
|
+
category: "games",
|
|
875
|
+
iconUrl: widgetIcon("snake"),
|
|
876
|
+
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
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.",
|
|
878
|
+
capabilities: [],
|
|
879
|
+
schema: z3.object({})
|
|
880
|
+
});
|
|
881
|
+
var flappyBirdMeta = defineFrameMeta({
|
|
882
|
+
name: "flappy-bird",
|
|
883
|
+
category: "games",
|
|
884
|
+
iconUrl: widgetIcon("flappy-bird"),
|
|
885
|
+
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
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.",
|
|
887
|
+
capabilities: [],
|
|
888
|
+
schema: z3.object({})
|
|
889
|
+
});
|
|
890
|
+
var videoMeta = defineFrameMeta({
|
|
891
|
+
name: "video",
|
|
892
|
+
category: "layout",
|
|
893
|
+
iconUrl: widgetIcon("video"),
|
|
894
|
+
layout: { w: 4, h: 3, minW: 2, minH: 2 },
|
|
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.",
|
|
896
|
+
capabilities: [],
|
|
897
|
+
schema: z3.object({
|
|
898
|
+
url: z3.string().min(1).describe(
|
|
899
|
+
"Video URL \u2014 a YouTube watch/share link, a Vimeo link, or a direct embeddable URL (https)."
|
|
900
|
+
),
|
|
901
|
+
title: z3.string().default("Video").describe("Accessible title for the embedded player (iframe title).")
|
|
902
|
+
})
|
|
903
|
+
});
|
|
904
|
+
var drawdyMeta = defineFrameMeta({
|
|
905
|
+
name: "drawdy",
|
|
906
|
+
category: "layout",
|
|
907
|
+
iconUrl: widgetIcon("drawdy"),
|
|
908
|
+
layout: { w: 8, h: 6, minW: 2, minH: 2 },
|
|
909
|
+
description: "Embeds drawdy.io as an interactive whiteboard canvas. No configuration needed.",
|
|
910
|
+
capabilities: [],
|
|
911
|
+
schema: z3.object({})
|
|
912
|
+
});
|
|
913
|
+
var countdownMeta = defineFrameMeta({
|
|
914
|
+
name: "countdown",
|
|
915
|
+
category: "tools",
|
|
916
|
+
iconUrl: widgetIcon("countdown"),
|
|
917
|
+
layout: { w: 3, h: 2, minW: 2, minH: 1 },
|
|
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.",
|
|
919
|
+
capabilities: [],
|
|
920
|
+
schema: z3.object({
|
|
921
|
+
target: z3.string().default("").describe(
|
|
922
|
+
`The moment to count down to, as an ISO 8601 string. Add a timezone for an unambiguous instant, e.g. "2026-07-30T18:00:00-04:00" or "2026-12-31T23:59:59Z"; a bare "2026-07-30T18:00" is read in the viewer's local timezone. Empty shows a "set a target" prompt.`
|
|
923
|
+
),
|
|
924
|
+
label: z3.string().default("").describe(
|
|
925
|
+
'Caption above the countdown, e.g. "FOMC Decision". Empty hides it.'
|
|
926
|
+
),
|
|
927
|
+
showTarget: z3.boolean().default(true).describe("Show the formatted target date and time under the countdown.")
|
|
928
|
+
})
|
|
929
|
+
});
|
|
930
|
+
var linkGridMeta = defineFrameMeta({
|
|
931
|
+
name: "link-grid",
|
|
932
|
+
category: "tools",
|
|
933
|
+
iconUrl: widgetIcon("link-grid"),
|
|
934
|
+
layout: { w: 3, h: 2, minW: 2, minH: 1 },
|
|
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.",
|
|
936
|
+
capabilities: [],
|
|
937
|
+
schema: z3.object({
|
|
938
|
+
links: z3.array(
|
|
506
939
|
z3.object({
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
940
|
+
label: z3.string().min(1).describe('Tile caption, e.g. "TradingView".'),
|
|
941
|
+
url: z3.string().min(1).describe("Destination URL (https). Opens in a new tab."),
|
|
942
|
+
icon: z3.string().default("").describe(
|
|
943
|
+
`Optional icon override: an emoji (e.g. "\u{1F4C8}") or an https image URL. Empty uses the destination site's favicon, falling back to the label's first letter.`
|
|
510
944
|
)
|
|
511
945
|
})
|
|
512
|
-
).min(
|
|
946
|
+
).min(1).default([
|
|
947
|
+
{
|
|
948
|
+
label: "TradingView",
|
|
949
|
+
url: "https://www.tradingview.com",
|
|
950
|
+
icon: "\u{1F4C8}"
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
label: "Hyperliquid",
|
|
954
|
+
url: "https://app.hyperliquid.xyz",
|
|
955
|
+
icon: "\u26A1"
|
|
956
|
+
}
|
|
957
|
+
]).describe("The links to show as tiles. At least one."),
|
|
958
|
+
columns: z3.number().int().min(1).max(4).default(2).describe("How many tiles per row.")
|
|
959
|
+
})
|
|
960
|
+
});
|
|
961
|
+
var calculatorMeta = defineFrameMeta({
|
|
962
|
+
name: "calculator",
|
|
963
|
+
category: "tools",
|
|
964
|
+
iconUrl: widgetIcon("calculator"),
|
|
965
|
+
layout: { w: 3, h: 3, minW: 2, minH: 2 },
|
|
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.",
|
|
967
|
+
capabilities: [],
|
|
968
|
+
schema: z3.object({
|
|
969
|
+
account: z3.number().positive().default(1e4).describe("Account size used as the risk base, in the quote currency."),
|
|
970
|
+
riskPct: z3.number().positive().max(100).default(1).describe("Percent of the account risked on the trade, e.g. 1 = 1%."),
|
|
971
|
+
entry: z3.number().positive().default(100).describe("Entry price."),
|
|
972
|
+
stop: z3.number().positive().default(95).describe(
|
|
973
|
+
"Stop-loss price. Its distance from entry sets the per-unit risk; below entry = long, above = short."
|
|
974
|
+
),
|
|
975
|
+
currency: z3.string().default("$").describe("Currency symbol shown next to money values.")
|
|
976
|
+
})
|
|
977
|
+
});
|
|
978
|
+
var quoteMeta = defineFrameMeta({
|
|
979
|
+
name: "quote",
|
|
980
|
+
category: "layout",
|
|
981
|
+
iconUrl: widgetIcon("quote"),
|
|
982
|
+
layout: { w: 4, h: 2, minW: 2, minH: 1 },
|
|
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.',
|
|
984
|
+
capabilities: [],
|
|
985
|
+
schema: z3.object({
|
|
986
|
+
quotes: z3.array(z3.string().min(1)).min(1).default([
|
|
987
|
+
"Be fearful when others are greedy, and greedy when others are fearful. \u2014 Warren Buffett",
|
|
988
|
+
"The trend is your friend until the end when it bends.",
|
|
989
|
+
"Plan the trade, trade the plan."
|
|
990
|
+
]).describe(
|
|
991
|
+
"One or more quotes. With more than one, the frame rotates through them."
|
|
992
|
+
),
|
|
993
|
+
intervalSec: z3.number().int().min(0).default(12).describe(
|
|
994
|
+
"Seconds between rotations when there are multiple quotes. 0 shows the first quote, fixed."
|
|
995
|
+
)
|
|
996
|
+
})
|
|
997
|
+
});
|
|
998
|
+
var dividerMeta = defineFrameMeta({
|
|
999
|
+
name: "divider",
|
|
1000
|
+
category: "layout",
|
|
1001
|
+
iconUrl: widgetIcon("divider"),
|
|
1002
|
+
layout: { w: 12, h: 1, minW: 1, minH: 1 },
|
|
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.",
|
|
1004
|
+
capabilities: [],
|
|
1005
|
+
chrome: "bare",
|
|
1006
|
+
schema: z3.object({
|
|
1007
|
+
label: z3.string().default("").describe(
|
|
1008
|
+
"Optional text shown in the middle of the rule. Empty = a clean line."
|
|
1009
|
+
),
|
|
1010
|
+
orientation: z3.enum(["horizontal", "vertical"]).default("horizontal").describe(
|
|
1011
|
+
"Horizontal rule (spans the width) or vertical rule (spans the height)."
|
|
1012
|
+
),
|
|
1013
|
+
style: z3.enum(["solid", "dashed", "dotted"]).default("solid").describe("Line style.")
|
|
1014
|
+
})
|
|
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
|
+
});
|
|
1235
|
+
var btcFeesMeta = defineFrameMeta({
|
|
1236
|
+
name: "btc-fees",
|
|
1237
|
+
category: "bitcoin",
|
|
1238
|
+
iconUrl: widgetIcon("btc-fees"),
|
|
1239
|
+
layout: { w: 3, h: 2, minW: 2, minH: 2 },
|
|
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.",
|
|
1241
|
+
capabilities: ["btc-fees"],
|
|
1242
|
+
source: SOURCES.mempool,
|
|
1243
|
+
schema: z3.object({
|
|
1244
|
+
tiers: z3.array(z3.enum(["fastest", "halfHour", "hour", "economy", "minimum"])).min(1).max(5).default(["fastest", "halfHour", "hour", "economy"]).describe(
|
|
1245
|
+
'Which fee tiers to show, in order. "fastest" = next block, "halfHour"/"hour" = within ~30/60 min, "economy"/"minimum" = cheapest relayable.'
|
|
1246
|
+
)
|
|
1247
|
+
})
|
|
1248
|
+
});
|
|
1249
|
+
var btcMempoolMeta = defineFrameMeta({
|
|
1250
|
+
name: "btc-mempool",
|
|
1251
|
+
category: "bitcoin",
|
|
1252
|
+
iconUrl: widgetIcon("btc-mempool"),
|
|
1253
|
+
layout: { w: 5, h: 3, minW: 3, minH: 2 },
|
|
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.",
|
|
1255
|
+
capabilities: ["btc-mempool"],
|
|
1256
|
+
source: SOURCES.mempool,
|
|
1257
|
+
schema: z3.object({
|
|
1258
|
+
projectedBlocks: z3.number().int().min(1).max(8).default(5).describe(
|
|
1259
|
+
"How many projected (yet-to-be-mined) blocks to show, next-to-mine first."
|
|
1260
|
+
)
|
|
1261
|
+
})
|
|
1262
|
+
});
|
|
1263
|
+
var btcBlocksMeta = defineFrameMeta({
|
|
1264
|
+
name: "btc-blocks",
|
|
1265
|
+
category: "bitcoin",
|
|
1266
|
+
iconUrl: widgetIcon("btc-blocks"),
|
|
1267
|
+
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
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.",
|
|
1269
|
+
capabilities: ["btc-blocks"],
|
|
1270
|
+
source: SOURCES.mempool,
|
|
1271
|
+
schema: z3.object({
|
|
1272
|
+
count: z3.number().int().min(3).max(15).default(8).describe("How many recent blocks to list (newest first).")
|
|
1273
|
+
})
|
|
1274
|
+
});
|
|
1275
|
+
var btcHashrateMeta = defineFrameMeta({
|
|
1276
|
+
name: "btc-hashrate",
|
|
1277
|
+
category: "bitcoin",
|
|
1278
|
+
iconUrl: widgetIcon("btc-hashrate"),
|
|
1279
|
+
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
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.",
|
|
1281
|
+
capabilities: ["btc-hashrate"],
|
|
1282
|
+
source: SOURCES.mempool,
|
|
1283
|
+
schema: z3.object({
|
|
1284
|
+
window: z3.enum(["1y", "2y", "3y"]).default("1y").describe("History window for the hashrate line.")
|
|
1285
|
+
})
|
|
1286
|
+
});
|
|
1287
|
+
var btcDifficultyMeta = defineFrameMeta({
|
|
1288
|
+
name: "btc-difficulty",
|
|
1289
|
+
category: "bitcoin",
|
|
1290
|
+
iconUrl: widgetIcon("btc-difficulty"),
|
|
1291
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
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.",
|
|
1293
|
+
capabilities: ["btc-difficulty"],
|
|
1294
|
+
source: SOURCES.mempool,
|
|
1295
|
+
schema: z3.object({
|
|
1296
|
+
showPrevious: z3.boolean().default(true).describe(
|
|
1297
|
+
"Also show the percentage change applied at the previous retarget."
|
|
1298
|
+
)
|
|
1299
|
+
})
|
|
1300
|
+
});
|
|
1301
|
+
var miningPoolsMeta = defineFrameMeta({
|
|
1302
|
+
name: "mining-pools",
|
|
1303
|
+
category: "bitcoin",
|
|
1304
|
+
iconUrl: widgetIcon("mining-pools"),
|
|
1305
|
+
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
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.",
|
|
1307
|
+
capabilities: ["mining-pools"],
|
|
1308
|
+
source: SOURCES.mempool,
|
|
1309
|
+
schema: z3.object({
|
|
1310
|
+
window: z3.enum(["24h", "3d", "1w", "1m"]).default("1w").describe("Window over which to measure each pool's block share."),
|
|
1311
|
+
topN: z3.number().int().min(3).max(25).default(12).describe(
|
|
1312
|
+
"How many of the largest pools to show; the rest fold into 'Other'."
|
|
1313
|
+
)
|
|
1314
|
+
})
|
|
1315
|
+
});
|
|
1316
|
+
var lightningStatsMeta = defineFrameMeta({
|
|
1317
|
+
name: "lightning-stats",
|
|
1318
|
+
category: "bitcoin",
|
|
1319
|
+
iconUrl: widgetIcon("lightning-stats"),
|
|
1320
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
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.",
|
|
1322
|
+
capabilities: ["lightning-stats"],
|
|
1323
|
+
source: SOURCES.mempool,
|
|
1324
|
+
schema: z3.object({
|
|
1325
|
+
showSplit: z3.boolean().default(true).describe(
|
|
1326
|
+
"Show the Tor vs clearnet node split under the headline stats."
|
|
1327
|
+
)
|
|
1328
|
+
})
|
|
1329
|
+
});
|
|
1330
|
+
var optionsPutCallMeta = defineFrameMeta({
|
|
1331
|
+
name: "options-put-call",
|
|
1332
|
+
category: "derivatives",
|
|
1333
|
+
iconUrl: widgetIcon("options-put-call"),
|
|
1334
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
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.",
|
|
1336
|
+
capabilities: ["options-summary"],
|
|
1337
|
+
source: SOURCES.deribit,
|
|
1338
|
+
schema: z3.object({
|
|
1339
|
+
currency: z3.enum(["BTC", "ETH"]).default("BTC").describe(
|
|
1340
|
+
"Which Deribit options book to summarise \u2014 BTC or ETH (the only deeply liquid books)."
|
|
1341
|
+
),
|
|
1342
|
+
basis: z3.enum(["oi", "volume"]).default("oi").describe(
|
|
1343
|
+
'Headline put/call ratio basis: "oi" = by open interest (positioning), "volume" = by 24h traded volume (flow). The other is shown smaller.'
|
|
1344
|
+
)
|
|
1345
|
+
})
|
|
1346
|
+
});
|
|
1347
|
+
var optionsIvMeta = defineFrameMeta({
|
|
1348
|
+
name: "options-iv",
|
|
1349
|
+
category: "derivatives",
|
|
1350
|
+
iconUrl: widgetIcon("options-iv"),
|
|
1351
|
+
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
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.",
|
|
1353
|
+
capabilities: ["volatility-index"],
|
|
1354
|
+
source: SOURCES.deribit,
|
|
1355
|
+
schema: z3.object({
|
|
1356
|
+
currency: z3.enum(["BTC", "ETH"]).default("BTC").describe("Which DVOL volatility index to plot \u2014 BTC or ETH."),
|
|
1357
|
+
lookback: z3.enum(["7D", "1M", "3M"]).default("1M").describe("History window for the volatility-index line.")
|
|
1358
|
+
})
|
|
1359
|
+
});
|
|
1360
|
+
var optionsOiStrikeMeta = defineFrameMeta({
|
|
1361
|
+
name: "options-oi-strike",
|
|
1362
|
+
category: "derivatives",
|
|
1363
|
+
iconUrl: widgetIcon("options-oi-strike"),
|
|
1364
|
+
layout: { w: 6, h: 4, minW: 4, minH: 3 },
|
|
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.",
|
|
1366
|
+
capabilities: ["options-summary"],
|
|
1367
|
+
source: SOURCES.deribit,
|
|
1368
|
+
schema: z3.object({
|
|
1369
|
+
currency: z3.enum(["BTC", "ETH"]).default("BTC").describe("Which Deribit options book \u2014 BTC or ETH."),
|
|
1370
|
+
strikes: z3.number().int().min(6).max(30).default(14).describe(
|
|
1371
|
+
"How many strikes nearest the current spot to show (centered on the underlying price)."
|
|
1372
|
+
)
|
|
1373
|
+
})
|
|
1374
|
+
});
|
|
1375
|
+
var coinMoversMeta = defineFrameMeta({
|
|
1376
|
+
name: "coin-movers",
|
|
1377
|
+
category: "markets",
|
|
1378
|
+
iconUrl: widgetIcon("coin-movers"),
|
|
1379
|
+
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
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.",
|
|
1381
|
+
capabilities: ["coin-movers"],
|
|
1382
|
+
source: SOURCES.coinpaprika,
|
|
1383
|
+
schema: z3.object({
|
|
1384
|
+
window: z3.enum(["1h", "24h", "7d", "30d"]).default("24h").describe(
|
|
1385
|
+
"Which price-change window ranks the movers: 1h (intraday momentum), 24h (daily), 7d (weekly), 30d (monthly)."
|
|
1386
|
+
),
|
|
1387
|
+
count: z3.number().int().min(3).max(15).default(6).describe("How many gainers and how many losers to list (each side)."),
|
|
1388
|
+
minRank: z3.number().int().min(20).max(300).default(150).describe(
|
|
1389
|
+
"Only consider coins ranked at or above this market-cap rank \u2014 a liquidity floor that keeps illiquid micro-cap dust (which posts absurd % moves on no volume) out of the list. Lower = stricter (megacaps only); higher = includes more small-caps."
|
|
1390
|
+
)
|
|
513
1391
|
})
|
|
514
1392
|
});
|
|
515
1393
|
var frameMetas = [
|
|
516
|
-
|
|
1394
|
+
newsFeedMeta,
|
|
1395
|
+
portfolioValueMeta,
|
|
1396
|
+
portfolioAllocationMeta,
|
|
1397
|
+
portfolioHoldingsMeta,
|
|
517
1398
|
bitcoinDominanceMeta,
|
|
518
1399
|
clockMeta,
|
|
519
1400
|
dailyAnalysisMeta,
|
|
@@ -521,6 +1402,10 @@ var frameMetas = [
|
|
|
521
1402
|
fearGreedMeta,
|
|
522
1403
|
filingsFeedMeta,
|
|
523
1404
|
fundamentalsMeta,
|
|
1405
|
+
financialStressMeta,
|
|
1406
|
+
laborMarketMeta,
|
|
1407
|
+
nationalDebtMeta,
|
|
1408
|
+
treasuryAuctionsMeta,
|
|
524
1409
|
fundingHeatmapMeta,
|
|
525
1410
|
fundingRateChartMeta,
|
|
526
1411
|
headingMeta,
|
|
@@ -536,7 +1421,34 @@ var frameMetas = [
|
|
|
536
1421
|
shortVolumeMeta,
|
|
537
1422
|
topMoversMeta,
|
|
538
1423
|
tvlTreemapMeta,
|
|
539
|
-
yieldCurveMeta
|
|
1424
|
+
yieldCurveMeta,
|
|
1425
|
+
dexVolumeTreemapMeta,
|
|
1426
|
+
dexVolumeChartMeta,
|
|
1427
|
+
protocolTvlTreemapMeta,
|
|
1428
|
+
protocolTvlChartMeta,
|
|
1429
|
+
protocolFeesTreemapMeta,
|
|
1430
|
+
marketCapTreemapMeta,
|
|
1431
|
+
openInterestMeta,
|
|
1432
|
+
snakeMeta,
|
|
1433
|
+
flappyBirdMeta,
|
|
1434
|
+
videoMeta,
|
|
1435
|
+
drawdyMeta,
|
|
1436
|
+
countdownMeta,
|
|
1437
|
+
linkGridMeta,
|
|
1438
|
+
calculatorMeta,
|
|
1439
|
+
quoteMeta,
|
|
1440
|
+
dividerMeta,
|
|
1441
|
+
btcFeesMeta,
|
|
1442
|
+
btcMempoolMeta,
|
|
1443
|
+
btcBlocksMeta,
|
|
1444
|
+
btcHashrateMeta,
|
|
1445
|
+
btcDifficultyMeta,
|
|
1446
|
+
miningPoolsMeta,
|
|
1447
|
+
lightningStatsMeta,
|
|
1448
|
+
optionsPutCallMeta,
|
|
1449
|
+
optionsIvMeta,
|
|
1450
|
+
optionsOiStrikeMeta,
|
|
1451
|
+
coinMoversMeta
|
|
540
1452
|
];
|
|
541
1453
|
|
|
542
1454
|
// src/init.ts
|
|
@@ -582,9 +1494,21 @@ function skeleton(title, author) {
|
|
|
582
1494
|
background: {
|
|
583
1495
|
type: "unicorn",
|
|
584
1496
|
projectId: "YrTzGatwjK7EoFpCSfgZ",
|
|
585
|
-
opacity:
|
|
1497
|
+
opacity: 1
|
|
1498
|
+
},
|
|
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
|
|
586
1511
|
},
|
|
587
|
-
theme: { accentHue: 242, accentSat: 90 },
|
|
588
1512
|
appearance: {
|
|
589
1513
|
radius: 18,
|
|
590
1514
|
borderStrength: 0.22,
|
|
@@ -643,17 +1567,84 @@ function init(args) {
|
|
|
643
1567
|
return 0;
|
|
644
1568
|
}
|
|
645
1569
|
|
|
1570
|
+
// src/lint.ts
|
|
1571
|
+
function lintSpec(spec) {
|
|
1572
|
+
const issues = [];
|
|
1573
|
+
const metaByName = new Map(frameMetas.map((meta) => [meta.name, meta]));
|
|
1574
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1575
|
+
for (const instance of spec.frames) {
|
|
1576
|
+
if (seenIds.has(instance.id))
|
|
1577
|
+
issues.push({
|
|
1578
|
+
frameId: instance.id,
|
|
1579
|
+
message: `duplicate frame id "${instance.id}"`
|
|
1580
|
+
});
|
|
1581
|
+
seenIds.add(instance.id);
|
|
1582
|
+
const meta = metaByName.get(instance.frame);
|
|
1583
|
+
if (!meta) {
|
|
1584
|
+
issues.push({
|
|
1585
|
+
frameId: instance.id,
|
|
1586
|
+
message: `unknown frame "${instance.frame}". available: ${[
|
|
1587
|
+
...metaByName.keys()
|
|
1588
|
+
].join(", ")}`
|
|
1589
|
+
});
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
const parsed = meta.schema.safeParse(instance.config);
|
|
1593
|
+
if (!parsed.success) {
|
|
1594
|
+
for (const issue of parsed.error.issues) {
|
|
1595
|
+
issues.push({
|
|
1596
|
+
frameId: instance.id,
|
|
1597
|
+
message: `config.${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
if (instance.position.x + instance.position.w > spec.grid.columns)
|
|
1602
|
+
issues.push({
|
|
1603
|
+
frameId: instance.id,
|
|
1604
|
+
message: `overflows the grid: x(${instance.position.x}) + w(${instance.position.w}) > ${spec.grid.columns} columns`
|
|
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
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
for (let i = 0; i < spec.frames.length; i++) {
|
|
1614
|
+
for (let j = i + 1; j < spec.frames.length; j++) {
|
|
1615
|
+
const a = spec.frames[i].position;
|
|
1616
|
+
const b = spec.frames[j].position;
|
|
1617
|
+
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;
|
|
1618
|
+
if (overlap)
|
|
1619
|
+
issues.push({
|
|
1620
|
+
frameId: spec.frames[i].id,
|
|
1621
|
+
message: `overlaps frame "${spec.frames[j].id}"`
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
return issues;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
646
1628
|
// src/serve.ts
|
|
647
|
-
import {
|
|
1629
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
648
1630
|
import { createServer } from "http";
|
|
649
|
-
import {
|
|
1631
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
650
1632
|
import { fileURLToPath } from "url";
|
|
1633
|
+
import sirv from "sirv";
|
|
651
1634
|
|
|
652
1635
|
// ../core/src/serve.ts
|
|
653
1636
|
import { readFile, writeFile } from "fs/promises";
|
|
1637
|
+
|
|
1638
|
+
// ../core/src/routes.ts
|
|
654
1639
|
var DASHBOARD_READ_ROUTE = "/__zframes/dashboard.json";
|
|
655
1640
|
var DASHBOARD_WRITE_ROUTE = "/__zframes/dashboard";
|
|
656
1641
|
var DASHBOARD_PROXY_ROUTE = "/__zframes/proxy";
|
|
1642
|
+
var ACCOUNT_PORTFOLIO_ROUTE = "/__zframes/account/portfolio";
|
|
1643
|
+
var ACCOUNT_CREDENTIALS_ROUTE = "/__zframes/account/credentials";
|
|
1644
|
+
var AGENTS_LIST_ROUTE = "/__zframes/agents";
|
|
1645
|
+
var ASK_ROUTE = "/__zframes/ask";
|
|
1646
|
+
|
|
1647
|
+
// ../core/src/serve.ts
|
|
657
1648
|
var MAX_BODY_BYTES = 2e6;
|
|
658
1649
|
var PROXY_ALLOW_HOSTS = /* @__PURE__ */ new Set([
|
|
659
1650
|
"data.sec.gov",
|
|
@@ -666,7 +1657,15 @@ var PROXY_ALLOW_HOSTS = /* @__PURE__ */ new Set([
|
|
|
666
1657
|
"markets.newyorkfed.org",
|
|
667
1658
|
"api.fiscaldata.treasury.gov",
|
|
668
1659
|
"api.bls.gov",
|
|
669
|
-
"cdn.finra.org"
|
|
1660
|
+
"cdn.finra.org",
|
|
1661
|
+
// News-outlet RSS feeds (CORS-blocked, so the news-feed frame reads them
|
|
1662
|
+
// through here). Headlines + links only; no keys.
|
|
1663
|
+
"www.coindesk.com",
|
|
1664
|
+
"cointelegraph.com",
|
|
1665
|
+
"decrypt.co",
|
|
1666
|
+
"www.cnbc.com",
|
|
1667
|
+
"www.nasdaq.com",
|
|
1668
|
+
"news.google.com"
|
|
670
1669
|
]);
|
|
671
1670
|
var PROXY_MAX_BYTES = 16e6;
|
|
672
1671
|
var PROXY_TIMEOUT_MS = 2e4;
|
|
@@ -708,8 +1707,8 @@ function handleSpecWrite(req, res, absFile) {
|
|
|
708
1707
|
req.on("end", async () => {
|
|
709
1708
|
if (aborted) return;
|
|
710
1709
|
try {
|
|
711
|
-
const
|
|
712
|
-
await writeFile(absFile, `${JSON.stringify(
|
|
1710
|
+
const json2 = JSON.parse(body);
|
|
1711
|
+
await writeFile(absFile, `${JSON.stringify(json2, null, 2)}
|
|
713
1712
|
`, "utf8");
|
|
714
1713
|
res.statusCode = 200;
|
|
715
1714
|
res.setHeader("content-type", "application/json");
|
|
@@ -780,18 +1779,61 @@ import { access, readFile as readFile2 } from "fs/promises";
|
|
|
780
1779
|
import { constants } from "fs";
|
|
781
1780
|
import { tmpdir } from "os";
|
|
782
1781
|
import { dirname as dirname2, join as join2 } from "path";
|
|
783
|
-
var AGENTS_LIST_ROUTE = "/__zframes/agents";
|
|
784
|
-
var ASK_ROUTE = "/__zframes/ask";
|
|
785
1782
|
var MAX_BODY_BYTES2 = 64e3;
|
|
786
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
|
+
}
|
|
787
1818
|
var RUNNERS = [
|
|
788
1819
|
{
|
|
789
1820
|
id: "claude",
|
|
790
1821
|
label: "Claude",
|
|
791
1822
|
bin: "claude",
|
|
792
|
-
// -p/--print is non-interactive
|
|
793
|
-
|
|
794
|
-
|
|
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)
|
|
795
1837
|
},
|
|
796
1838
|
{
|
|
797
1839
|
id: "codex",
|
|
@@ -848,7 +1890,7 @@ function detectAgents() {
|
|
|
848
1890
|
}
|
|
849
1891
|
return detected;
|
|
850
1892
|
}
|
|
851
|
-
async function buildPrompt(specFile, question) {
|
|
1893
|
+
async function buildPrompt(specFile, question, clientContext, catalogue, history) {
|
|
852
1894
|
let title = "a live market dashboard";
|
|
853
1895
|
const symbols = /* @__PURE__ */ new Set();
|
|
854
1896
|
try {
|
|
@@ -863,13 +1905,40 @@ async function buildPrompt(specFile, question) {
|
|
|
863
1905
|
}
|
|
864
1906
|
} catch {
|
|
865
1907
|
}
|
|
866
|
-
const
|
|
867
|
-
|
|
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")}
|
|
868
1925
|
|
|
869
|
-
|
|
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.
|
|
1936
|
+
|
|
1937
|
+
Question: ${question}`
|
|
1938
|
+
);
|
|
870
1939
|
}
|
|
871
1940
|
var askCounter = 0;
|
|
872
|
-
function runAgent(runner, prompt, cwd) {
|
|
1941
|
+
function runAgent(runner, prompt, cwd, onDelta) {
|
|
873
1942
|
const outFile = join2(
|
|
874
1943
|
tmpdir(),
|
|
875
1944
|
`zframes-ask-${process.pid}-${++askCounter}.txt`
|
|
@@ -877,13 +1946,17 @@ function runAgent(runner, prompt, cwd) {
|
|
|
877
1946
|
return new Promise((resolve4) => {
|
|
878
1947
|
let child;
|
|
879
1948
|
try {
|
|
880
|
-
child = spawn(runner.bin, runner.buildArgs(prompt, outFile), {
|
|
1949
|
+
child = spawn(runner.bin, runner.buildArgs(prompt, outFile), {
|
|
1950
|
+
cwd,
|
|
1951
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1952
|
+
});
|
|
881
1953
|
} catch (error) {
|
|
882
1954
|
resolve4({ ok: false, error: String(error) });
|
|
883
1955
|
return;
|
|
884
1956
|
}
|
|
885
1957
|
let stdout = "";
|
|
886
1958
|
let stderr = "";
|
|
1959
|
+
let lineBuf = "";
|
|
887
1960
|
let settled = false;
|
|
888
1961
|
const finish = (r) => {
|
|
889
1962
|
if (settled) return;
|
|
@@ -895,13 +1968,28 @@ function runAgent(runner, prompt, cwd) {
|
|
|
895
1968
|
child.kill("SIGKILL");
|
|
896
1969
|
finish({ ok: false, error: `${runner.label} timed out` });
|
|
897
1970
|
}, RUN_TIMEOUT_MS);
|
|
898
|
-
|
|
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
|
+
});
|
|
899
1986
|
child.stderr?.on("data", (d) => stderr += d);
|
|
900
1987
|
child.on(
|
|
901
1988
|
"error",
|
|
902
1989
|
(e) => finish({ ok: false, error: String(e.message) })
|
|
903
1990
|
);
|
|
904
1991
|
child.on("close", (code) => {
|
|
1992
|
+
if (streaming && lineBuf) emit(lineBuf);
|
|
905
1993
|
if (code !== 0) {
|
|
906
1994
|
finish({
|
|
907
1995
|
ok: false,
|
|
@@ -926,7 +2014,7 @@ async function handleAgents(res) {
|
|
|
926
2014
|
JSON.stringify({ agents: agents.map(({ id, label }) => ({ id, label })) })
|
|
927
2015
|
);
|
|
928
2016
|
}
|
|
929
|
-
function handleAsk(req, res, specFile) {
|
|
2017
|
+
function handleAsk(req, res, specFile, catalogue) {
|
|
930
2018
|
if (req.method !== "POST") {
|
|
931
2019
|
res.statusCode = 405;
|
|
932
2020
|
res.end();
|
|
@@ -951,59 +2039,292 @@ function handleAsk(req, res, specFile) {
|
|
|
951
2039
|
});
|
|
952
2040
|
req.on("end", async () => {
|
|
953
2041
|
if (aborted) return;
|
|
954
|
-
const
|
|
2042
|
+
const replyJson = (status, payload) => {
|
|
955
2043
|
res.statusCode = status;
|
|
956
2044
|
res.setHeader("content-type", "application/json");
|
|
957
2045
|
res.end(JSON.stringify(payload));
|
|
958
2046
|
};
|
|
959
2047
|
let question;
|
|
960
2048
|
let requested;
|
|
2049
|
+
let clientContext;
|
|
2050
|
+
let history;
|
|
961
2051
|
try {
|
|
962
2052
|
const parsed = JSON.parse(body);
|
|
963
2053
|
if (typeof parsed.question !== "string" || !parsed.question.trim())
|
|
964
2054
|
throw new Error("missing question");
|
|
965
2055
|
question = parsed.question.trim();
|
|
966
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);
|
|
967
2062
|
} catch (error) {
|
|
968
|
-
|
|
2063
|
+
replyJson(400, { ok: false, error: String(error.message) });
|
|
969
2064
|
return;
|
|
970
2065
|
}
|
|
971
2066
|
const agents = await detectAgents();
|
|
972
2067
|
if (agents.length === 0) {
|
|
973
|
-
|
|
2068
|
+
replyJson(503, {
|
|
974
2069
|
ok: false,
|
|
975
2070
|
error: "no agent CLI found \u2014 install claude, codex, or kimi"
|
|
976
2071
|
});
|
|
977
2072
|
return;
|
|
978
2073
|
}
|
|
979
2074
|
const runner = agents.find((a) => a.id === requested) ?? agents[0];
|
|
980
|
-
const prompt = await buildPrompt(
|
|
981
|
-
|
|
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
|
+
);
|
|
982
2099
|
if (result.ok)
|
|
983
|
-
|
|
984
|
-
else
|
|
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)
|
|
985
2322
|
});
|
|
986
2323
|
}
|
|
987
2324
|
|
|
988
2325
|
// src/serve.ts
|
|
989
2326
|
var DEFAULT_PORT = 37263;
|
|
990
|
-
var
|
|
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
|
-
};
|
|
2327
|
+
var FRAME_CATALOGUE = catalogueSummary(frameMetas);
|
|
1007
2328
|
function parseArgs2(args) {
|
|
1008
2329
|
let file = "dashboard.json";
|
|
1009
2330
|
let port = DEFAULT_PORT;
|
|
@@ -1029,31 +2350,6 @@ function parseArgs2(args) {
|
|
|
1029
2350
|
}
|
|
1030
2351
|
return { file, port, contact };
|
|
1031
2352
|
}
|
|
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
2353
|
function serve(args) {
|
|
1058
2354
|
const parsed = parseArgs2(args);
|
|
1059
2355
|
if ("error" in parsed) {
|
|
@@ -1071,11 +2367,14 @@ function serve(args) {
|
|
|
1071
2367
|
}
|
|
1072
2368
|
const userDir = resolve2(file, "..");
|
|
1073
2369
|
const bundleDir = fileURLToPath(new URL("../runtime", import.meta.url));
|
|
1074
|
-
if (!existsSync2(
|
|
2370
|
+
if (!existsSync2(join4(bundleDir, "index.html"))) {
|
|
1075
2371
|
console.error(`\u2717 runtime bundle missing at ${bundleDir}`);
|
|
1076
2372
|
console.error(" run `pnpm build:cli` to build it.");
|
|
1077
2373
|
return Promise.resolve(1);
|
|
1078
2374
|
}
|
|
2375
|
+
const serveBundle = sirv(bundleDir, { dev: true });
|
|
2376
|
+
const serveSiblings = sirv(userDir, { dev: true });
|
|
2377
|
+
const serveSpa = sirv(bundleDir, { dev: true, single: true });
|
|
1079
2378
|
return new Promise((done) => {
|
|
1080
2379
|
const server = createServer((req, res) => {
|
|
1081
2380
|
const rawPath = (req.url ?? "/").split("?")[0];
|
|
@@ -1110,7 +2409,15 @@ function serve(args) {
|
|
|
1110
2409
|
return;
|
|
1111
2410
|
}
|
|
1112
2411
|
if (path === ASK_ROUTE) {
|
|
1113
|
-
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);
|
|
1114
2421
|
return;
|
|
1115
2422
|
}
|
|
1116
2423
|
if (path === DASHBOARD_PROXY_ROUTE) {
|
|
@@ -1129,9 +2436,18 @@ function serve(args) {
|
|
|
1129
2436
|
res.end();
|
|
1130
2437
|
return;
|
|
1131
2438
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
2439
|
+
serveBundle(
|
|
2440
|
+
req,
|
|
2441
|
+
res,
|
|
2442
|
+
() => serveSiblings(
|
|
2443
|
+
req,
|
|
2444
|
+
res,
|
|
2445
|
+
() => serveSpa(req, res, () => {
|
|
2446
|
+
res.statusCode = 404;
|
|
2447
|
+
res.end();
|
|
2448
|
+
})
|
|
2449
|
+
)
|
|
2450
|
+
);
|
|
1135
2451
|
});
|
|
1136
2452
|
server.on("error", (err) => {
|
|
1137
2453
|
if (err.code === "EADDRINUSE") {
|
|
@@ -1144,7 +2460,7 @@ function serve(args) {
|
|
|
1144
2460
|
done(1);
|
|
1145
2461
|
});
|
|
1146
2462
|
server.listen(parsed.port, "127.0.0.1", () => {
|
|
1147
|
-
const url = `http://
|
|
2463
|
+
const url = `http://localhost:${parsed.port}`;
|
|
1148
2464
|
console.log(`\u26A1 zframes is live at ${url}`);
|
|
1149
2465
|
console.log(
|
|
1150
2466
|
` serving ${parsed.file} \u2014 live editing on; drag, resize, then Save writes back.`
|
|
@@ -1157,107 +2473,308 @@ function serve(args) {
|
|
|
1157
2473
|
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
1158
2474
|
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
1159
2475
|
|
|
2476
|
+
// ../core/src/cache.ts
|
|
2477
|
+
var TtlCache = class {
|
|
2478
|
+
entries = /* @__PURE__ */ new Map();
|
|
2479
|
+
inflight = /* @__PURE__ */ new Map();
|
|
2480
|
+
namespace;
|
|
2481
|
+
ttlMs;
|
|
2482
|
+
persist;
|
|
2483
|
+
staleOnError;
|
|
2484
|
+
revive;
|
|
2485
|
+
constructor(options) {
|
|
2486
|
+
this.namespace = options.namespace;
|
|
2487
|
+
this.ttlMs = options.ttlMs;
|
|
2488
|
+
this.persist = options.persist ?? false;
|
|
2489
|
+
this.staleOnError = options.staleOnError ?? true;
|
|
2490
|
+
this.revive = options.revive;
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Resolve the value for `key`, calling `load` only on a miss or a stale entry.
|
|
2494
|
+
* Serves a fresh cached value with no network call, coalesces concurrent loads
|
|
2495
|
+
* onto one promise, and (by default) serves the last good value if `load` throws.
|
|
2496
|
+
*/
|
|
2497
|
+
get(key, load) {
|
|
2498
|
+
const entry = this.read(key);
|
|
2499
|
+
if (entry && Date.now() - entry.at < this.ttlMs)
|
|
2500
|
+
return Promise.resolve(entry.value);
|
|
2501
|
+
const pending = this.inflight.get(key);
|
|
2502
|
+
if (pending) return pending;
|
|
2503
|
+
const promise = load().then((value) => {
|
|
2504
|
+
this.write(key, value);
|
|
2505
|
+
return value;
|
|
2506
|
+
}).catch((error) => {
|
|
2507
|
+
if (this.staleOnError && entry) return entry.value;
|
|
2508
|
+
throw error;
|
|
2509
|
+
}).finally(() => {
|
|
2510
|
+
this.inflight.delete(key);
|
|
2511
|
+
});
|
|
2512
|
+
this.inflight.set(key, promise);
|
|
2513
|
+
return promise;
|
|
2514
|
+
}
|
|
2515
|
+
/** Hydrate from memo, falling back to localStorage on a cold (persisted) read. */
|
|
2516
|
+
read(key) {
|
|
2517
|
+
const memo = this.entries.get(key);
|
|
2518
|
+
if (memo) return memo;
|
|
2519
|
+
if (!this.persist || typeof localStorage === "undefined") return null;
|
|
2520
|
+
try {
|
|
2521
|
+
const raw = localStorage.getItem(this.storageKey(key));
|
|
2522
|
+
if (!raw) return null;
|
|
2523
|
+
const parsed = JSON.parse(raw);
|
|
2524
|
+
if (typeof parsed?.at !== "number") return null;
|
|
2525
|
+
let value;
|
|
2526
|
+
if (this.revive) {
|
|
2527
|
+
const revived = this.revive(parsed.value);
|
|
2528
|
+
if (revived === null) return null;
|
|
2529
|
+
value = revived;
|
|
2530
|
+
} else {
|
|
2531
|
+
value = parsed.value;
|
|
2532
|
+
}
|
|
2533
|
+
const entry = { at: parsed.at, value };
|
|
2534
|
+
this.entries.set(key, entry);
|
|
2535
|
+
return entry;
|
|
2536
|
+
} catch {
|
|
2537
|
+
return null;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
write(key, value) {
|
|
2541
|
+
const entry = { at: Date.now(), value };
|
|
2542
|
+
this.entries.set(key, entry);
|
|
2543
|
+
if (!this.persist || typeof localStorage === "undefined") return;
|
|
2544
|
+
try {
|
|
2545
|
+
localStorage.setItem(this.storageKey(key), JSON.stringify(entry));
|
|
2546
|
+
} catch {
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
storageKey(key) {
|
|
2550
|
+
return `${this.namespace}:${key}`;
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
|
|
1160
2554
|
// ../core/src/fetch.ts
|
|
1161
2555
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
1162
2556
|
var USER_AGENT = "zframes (+https://github.com/zentryhq/zframes)";
|
|
1163
|
-
|
|
1164
|
-
async function fetchJson(url, schema, { timeoutMs = DEFAULT_TIMEOUT_MS, init: init2, proxied } = {}) {
|
|
2557
|
+
async function request(url, { timeoutMs = DEFAULT_TIMEOUT_MS, init: init2, proxied }) {
|
|
1165
2558
|
const headers = new Headers(init2?.headers);
|
|
1166
2559
|
if (typeof document === "undefined" && !headers.has("User-Agent")) {
|
|
1167
2560
|
headers.set("User-Agent", USER_AGENT);
|
|
1168
2561
|
}
|
|
1169
|
-
const target = proxied && typeof document !== "undefined" ? `${
|
|
2562
|
+
const target = proxied && typeof document !== "undefined" ? `${DASHBOARD_PROXY_ROUTE}?url=${encodeURIComponent(url)}` : url;
|
|
1170
2563
|
const res = await fetch(target, {
|
|
1171
2564
|
...init2,
|
|
1172
2565
|
headers,
|
|
1173
2566
|
signal: init2?.signal ?? AbortSignal.timeout(timeoutMs)
|
|
1174
2567
|
});
|
|
1175
2568
|
if (!res.ok) throw new Error(`${url} failed: ${res.status}`);
|
|
2569
|
+
return res;
|
|
2570
|
+
}
|
|
2571
|
+
async function fetchJson(url, schema, opts = {}) {
|
|
2572
|
+
const res = await request(url, opts);
|
|
1176
2573
|
const body = await res.json();
|
|
1177
2574
|
return schema ? schema.parse(body) : body;
|
|
1178
2575
|
}
|
|
1179
2576
|
|
|
1180
2577
|
// ../provider-alternativeme/src/index.ts
|
|
1181
2578
|
var FNG_URL = "https://api.alternative.me/fng/";
|
|
2579
|
+
var sentimentCache = new TtlCache({
|
|
2580
|
+
namespace: "zframes:alternativeme:fng",
|
|
2581
|
+
ttlMs: 45 * 6e4,
|
|
2582
|
+
persist: true,
|
|
2583
|
+
revive: (value) => Array.isArray(value) ? value : null
|
|
2584
|
+
});
|
|
1182
2585
|
var AlternativeMeProvider = class {
|
|
1183
2586
|
name = "alternative.me";
|
|
1184
2587
|
capabilities = ["sentiment"];
|
|
1185
2588
|
async getFearGreed(limit = 30) {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
2589
|
+
return sentimentCache.get(String(limit), async () => {
|
|
2590
|
+
const body = await fetchJson(`${FNG_URL}?limit=${limit}`);
|
|
2591
|
+
if (!Array.isArray(body?.data))
|
|
2592
|
+
throw new Error("alternative.me fng: unexpected response shape");
|
|
2593
|
+
return body.data.map((entry) => ({
|
|
2594
|
+
value: Number(entry.value),
|
|
2595
|
+
classification: entry.value_classification ?? "",
|
|
2596
|
+
time: Number(entry.timestamp) * 1e3
|
|
2597
|
+
})).filter(
|
|
2598
|
+
(point) => Number.isFinite(point.value) && Number.isFinite(point.time)
|
|
2599
|
+
);
|
|
2600
|
+
});
|
|
1196
2601
|
}
|
|
1197
2602
|
};
|
|
1198
2603
|
|
|
1199
2604
|
// ../provider-coingecko/src/index.ts
|
|
1200
2605
|
var GLOBAL_URL = "https://api.coingecko.com/api/v3/global";
|
|
1201
|
-
var
|
|
1202
|
-
var
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
} catch {
|
|
1215
|
-
}
|
|
1216
|
-
return null;
|
|
1217
|
-
}
|
|
1218
|
-
function writeCache(value) {
|
|
1219
|
-
memo = { at: Date.now(), value };
|
|
1220
|
-
try {
|
|
1221
|
-
if (typeof localStorage !== "undefined")
|
|
1222
|
-
localStorage.setItem(CACHE_KEY, JSON.stringify(memo));
|
|
1223
|
-
} catch {
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
2606
|
+
var MARKETS_URL = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=50&page=1&sparkline=false&price_change_percentage=24h";
|
|
2607
|
+
var globalCache = new TtlCache({
|
|
2608
|
+
namespace: "zframes:coingecko:global",
|
|
2609
|
+
ttlMs: 12 * 6e4,
|
|
2610
|
+
persist: true
|
|
2611
|
+
});
|
|
2612
|
+
var marketsCache = new TtlCache({
|
|
2613
|
+
namespace: "zframes:coingecko:markets",
|
|
2614
|
+
ttlMs: 10 * 6e4,
|
|
2615
|
+
persist: true
|
|
2616
|
+
});
|
|
1226
2617
|
var CoinGeckoProvider = class {
|
|
1227
2618
|
name = "coingecko";
|
|
1228
|
-
capabilities = [
|
|
2619
|
+
capabilities = [
|
|
2620
|
+
"global-market",
|
|
2621
|
+
"coin-markets"
|
|
2622
|
+
];
|
|
1229
2623
|
async getGlobalMarket() {
|
|
1230
|
-
|
|
1231
|
-
if (cached && Date.now() - cached.at < TTL_MS) return cached.value;
|
|
1232
|
-
try {
|
|
2624
|
+
return globalCache.get("global", async () => {
|
|
1233
2625
|
const body = await fetchJson(GLOBAL_URL);
|
|
1234
2626
|
if (!body?.data?.total_market_cap || !body.data.market_cap_percentage)
|
|
1235
2627
|
throw new Error("coingecko global: unexpected response shape");
|
|
1236
2628
|
const change = Number(body.data.market_cap_change_percentage_24h_usd);
|
|
1237
|
-
|
|
2629
|
+
return {
|
|
1238
2630
|
totalMarketCapUsd: body.data.total_market_cap.usd ?? 0,
|
|
1239
2631
|
marketCapChangePct24h: Number.isFinite(change) ? change : 0,
|
|
1240
2632
|
dominance: body.data.market_cap_percentage
|
|
1241
2633
|
};
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
async getCoinMarkets() {
|
|
2637
|
+
return marketsCache.get("markets", async () => {
|
|
2638
|
+
const body = await fetchJson(MARKETS_URL);
|
|
2639
|
+
if (!Array.isArray(body))
|
|
2640
|
+
throw new Error("coingecko markets: unexpected response shape");
|
|
2641
|
+
return body.filter((c) => Number.isFinite(c.market_cap) && c.market_cap > 0).map((c) => ({
|
|
2642
|
+
symbol: (c.symbol ?? "").toUpperCase(),
|
|
2643
|
+
name: c.name,
|
|
2644
|
+
marketCapUsd: c.market_cap,
|
|
2645
|
+
changePct24h: Number.isFinite(c.price_change_percentage_24h) ? c.price_change_percentage_24h : void 0
|
|
2646
|
+
}));
|
|
2647
|
+
});
|
|
1248
2648
|
}
|
|
1249
2649
|
};
|
|
1250
2650
|
|
|
1251
2651
|
// ../provider-defillama/src/index.ts
|
|
1252
2652
|
var CHAINS_URL = "https://api.llama.fi/v2/chains";
|
|
2653
|
+
var DEXS_URL = "https://api.llama.fi/overview/dexs";
|
|
2654
|
+
var FEES_URL = "https://api.llama.fi/overview/fees";
|
|
2655
|
+
var PROTOCOLS_URL = "https://api.llama.fi/protocols";
|
|
2656
|
+
var SNAPSHOT_TTL_MS = 8 * 6e4;
|
|
2657
|
+
var HISTORY_TTL_MS = 30 * 6e4;
|
|
2658
|
+
var tvlCache = new TtlCache({
|
|
2659
|
+
namespace: "zframes:defillama:tvl",
|
|
2660
|
+
ttlMs: SNAPSHOT_TTL_MS
|
|
2661
|
+
});
|
|
2662
|
+
var dexVolumeCache = new TtlCache({
|
|
2663
|
+
namespace: "zframes:defillama:dex-volume",
|
|
2664
|
+
ttlMs: SNAPSHOT_TTL_MS
|
|
2665
|
+
});
|
|
2666
|
+
var protocolTvlCache = new TtlCache({
|
|
2667
|
+
namespace: "zframes:defillama:protocol-tvl",
|
|
2668
|
+
ttlMs: SNAPSHOT_TTL_MS
|
|
2669
|
+
});
|
|
2670
|
+
var protocolFeesCache = new TtlCache({
|
|
2671
|
+
namespace: "zframes:defillama:protocol-fees",
|
|
2672
|
+
ttlMs: SNAPSHOT_TTL_MS
|
|
2673
|
+
});
|
|
2674
|
+
var dexHistoryCache = new TtlCache({
|
|
2675
|
+
namespace: "zframes:defillama:dex-history",
|
|
2676
|
+
ttlMs: HISTORY_TTL_MS
|
|
2677
|
+
});
|
|
2678
|
+
var protocolHistoryCache = new TtlCache({
|
|
2679
|
+
namespace: "zframes:defillama:protocol-history",
|
|
2680
|
+
ttlMs: HISTORY_TTL_MS
|
|
2681
|
+
});
|
|
2682
|
+
var slugKey = (slugs) => [...slugs].sort().join(",");
|
|
2683
|
+
function toSeries(chart) {
|
|
2684
|
+
if (!Array.isArray(chart)) return [];
|
|
2685
|
+
return chart.map(([ts, value]) => ({ time: ts * 1e3, value: Number(value) })).filter((p) => Number.isFinite(p.time) && Number.isFinite(p.value));
|
|
2686
|
+
}
|
|
2687
|
+
var changeOf = (v) => Number.isFinite(v) ? v : void 0;
|
|
1253
2688
|
var DefiLlamaProvider = class {
|
|
1254
2689
|
name = "defillama";
|
|
1255
|
-
capabilities = [
|
|
2690
|
+
capabilities = [
|
|
2691
|
+
"tvl",
|
|
2692
|
+
"dex-volume",
|
|
2693
|
+
"protocol-tvl",
|
|
2694
|
+
"protocol-fees"
|
|
2695
|
+
];
|
|
1256
2696
|
async getTvlByChain() {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
2697
|
+
return tvlCache.get("chains", async () => {
|
|
2698
|
+
const chains = await fetchJson(CHAINS_URL);
|
|
2699
|
+
if (!Array.isArray(chains))
|
|
2700
|
+
throw new Error("defillama chains: unexpected response shape");
|
|
2701
|
+
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 }));
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
async getDexVolume() {
|
|
2705
|
+
return dexVolumeCache.get("overview", async () => {
|
|
2706
|
+
const body = await fetchJson(DEXS_URL);
|
|
2707
|
+
const protocols = body?.protocols;
|
|
2708
|
+
if (!Array.isArray(protocols))
|
|
2709
|
+
throw new Error("defillama dexs: unexpected response shape");
|
|
2710
|
+
return protocols.filter((p) => Number.isFinite(p.total24h) && (p.total24h ?? 0) > 0).map((p) => ({
|
|
2711
|
+
name: p.name,
|
|
2712
|
+
volume24h: p.total24h,
|
|
2713
|
+
changePct: changeOf(p.change_1d)
|
|
2714
|
+
})).sort((a, b) => b.volume24h - a.volume24h);
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
async getDexVolumeHistory(slugs) {
|
|
2718
|
+
return dexHistoryCache.get(slugKey(slugs), async () => {
|
|
2719
|
+
const pairs = await Promise.all(
|
|
2720
|
+
slugs.map(
|
|
2721
|
+
(slug) => fetchJson(
|
|
2722
|
+
`https://api.llama.fi/summary/dexs/${encodeURIComponent(
|
|
2723
|
+
slug
|
|
2724
|
+
)}?excludeTotalDataChartBreakdown=true`
|
|
2725
|
+
).then((body) => [slug, toSeries(body.totalDataChart)]).catch(() => [slug, []])
|
|
2726
|
+
)
|
|
2727
|
+
);
|
|
2728
|
+
return Object.fromEntries(pairs);
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
async getProtocolTvl() {
|
|
2732
|
+
return protocolTvlCache.get("overview", async () => {
|
|
2733
|
+
const protocols = await fetchJson(PROTOCOLS_URL);
|
|
2734
|
+
if (!Array.isArray(protocols))
|
|
2735
|
+
throw new Error("defillama protocols: unexpected response shape");
|
|
2736
|
+
return protocols.filter((p) => Number.isFinite(p.tvl) && (p.tvl ?? 0) > 0).map((p) => ({
|
|
2737
|
+
name: p.name,
|
|
2738
|
+
tvl: p.tvl,
|
|
2739
|
+
category: p.category,
|
|
2740
|
+
changePct: changeOf(p.change_1d)
|
|
2741
|
+
})).sort((a, b) => b.tvl - a.tvl);
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
async getProtocolTvlHistory(slugs) {
|
|
2745
|
+
return protocolHistoryCache.get(slugKey(slugs), async () => {
|
|
2746
|
+
const pairs = await Promise.all(
|
|
2747
|
+
slugs.map(
|
|
2748
|
+
(slug) => fetchJson(
|
|
2749
|
+
`https://api.llama.fi/protocol/${encodeURIComponent(slug)}`
|
|
2750
|
+
).then(
|
|
2751
|
+
(body) => [
|
|
2752
|
+
slug,
|
|
2753
|
+
(body.tvl ?? []).map((p) => ({
|
|
2754
|
+
time: p.date * 1e3,
|
|
2755
|
+
value: Number(p.totalLiquidityUSD)
|
|
2756
|
+
})).filter(
|
|
2757
|
+
(p) => Number.isFinite(p.time) && Number.isFinite(p.value)
|
|
2758
|
+
)
|
|
2759
|
+
]
|
|
2760
|
+
).catch(() => [slug, []])
|
|
2761
|
+
)
|
|
2762
|
+
);
|
|
2763
|
+
return Object.fromEntries(pairs);
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
async getProtocolFees() {
|
|
2767
|
+
return protocolFeesCache.get("overview", async () => {
|
|
2768
|
+
const body = await fetchJson(FEES_URL);
|
|
2769
|
+
const protocols = body?.protocols;
|
|
2770
|
+
if (!Array.isArray(protocols))
|
|
2771
|
+
throw new Error("defillama fees: unexpected response shape");
|
|
2772
|
+
return protocols.filter((p) => Number.isFinite(p.total24h) && (p.total24h ?? 0) > 0).map((p) => ({
|
|
2773
|
+
name: p.name,
|
|
2774
|
+
fees24h: p.total24h,
|
|
2775
|
+
changePct: changeOf(p.change_1d)
|
|
2776
|
+
})).sort((a, b) => b.fees24h - a.fees24h);
|
|
2777
|
+
});
|
|
1261
2778
|
}
|
|
1262
2779
|
};
|
|
1263
2780
|
|
|
@@ -1280,7 +2797,8 @@ var HyperliquidProvider = class {
|
|
|
1280
2797
|
"quote-stream",
|
|
1281
2798
|
"day-stats",
|
|
1282
2799
|
"funding-history",
|
|
1283
|
-
"ohlcv"
|
|
2800
|
+
"ohlcv",
|
|
2801
|
+
"open-interest"
|
|
1284
2802
|
];
|
|
1285
2803
|
ws = null;
|
|
1286
2804
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -1339,6 +2857,39 @@ var HyperliquidProvider = class {
|
|
|
1339
2857
|
);
|
|
1340
2858
|
return out;
|
|
1341
2859
|
}
|
|
2860
|
+
async getOpenInterest(symbols) {
|
|
2861
|
+
const wholeDexes = /* @__PURE__ */ new Set();
|
|
2862
|
+
const concrete = /* @__PURE__ */ new Set();
|
|
2863
|
+
if (!symbols) {
|
|
2864
|
+
wholeDexes.add("");
|
|
2865
|
+
} else {
|
|
2866
|
+
for (const s of symbols) {
|
|
2867
|
+
if (s.endsWith(":*")) wholeDexes.add(s.slice(0, -2));
|
|
2868
|
+
else concrete.add(s);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
const dexes = new Set(wholeDexes);
|
|
2872
|
+
for (const s of concrete) dexes.add(dexOf(s));
|
|
2873
|
+
const out = [];
|
|
2874
|
+
await Promise.all(
|
|
2875
|
+
[...dexes].map(async (dex) => {
|
|
2876
|
+
const body = { type: "metaAndAssetCtxs" };
|
|
2877
|
+
if (dex) body.dex = dex;
|
|
2878
|
+
const [meta, ctxs] = await info(body);
|
|
2879
|
+
const wholeDex = wholeDexes.has(dex);
|
|
2880
|
+
meta.universe.forEach((asset, i) => {
|
|
2881
|
+
if (!wholeDex && !concrete.has(asset.name)) return;
|
|
2882
|
+
const ctx = ctxs[i];
|
|
2883
|
+
if (!ctx) return;
|
|
2884
|
+
const markPx = Number(ctx.markPx);
|
|
2885
|
+
const oi = Number(ctx.openInterest);
|
|
2886
|
+
if (!Number.isFinite(markPx) || !Number.isFinite(oi)) return;
|
|
2887
|
+
out.push({ symbol: asset.name, openInterestUsd: oi * markPx });
|
|
2888
|
+
});
|
|
2889
|
+
})
|
|
2890
|
+
);
|
|
2891
|
+
return out.sort((a, b) => b.openInterestUsd - a.openInterestUsd);
|
|
2892
|
+
}
|
|
1342
2893
|
async getFundingHistory(symbols, startTimeMs) {
|
|
1343
2894
|
const results = await Promise.all(
|
|
1344
2895
|
symbols.map(async (coin) => {
|
|
@@ -1434,14 +2985,14 @@ function loadSpec(file) {
|
|
|
1434
2985
|
console.error(`\u2717 cannot read ${file}`);
|
|
1435
2986
|
return null;
|
|
1436
2987
|
}
|
|
1437
|
-
let
|
|
2988
|
+
let json2;
|
|
1438
2989
|
try {
|
|
1439
|
-
|
|
2990
|
+
json2 = JSON.parse(raw);
|
|
1440
2991
|
} catch (error) {
|
|
1441
2992
|
console.error(`\u2717 ${file} is not valid JSON: ${error.message}`);
|
|
1442
2993
|
return null;
|
|
1443
2994
|
}
|
|
1444
|
-
const parsed = DashboardSpecSchema.safeParse(
|
|
2995
|
+
const parsed = DashboardSpecSchema.safeParse(json2);
|
|
1445
2996
|
if (!parsed.success) {
|
|
1446
2997
|
console.error(`\u2717 ${file} is not a valid dashboard spec:`);
|
|
1447
2998
|
for (const issue of parsed.error.issues)
|
|
@@ -1506,8 +3057,8 @@ function loadPriorEntry(dashboardPath, logFlag) {
|
|
|
1506
3057
|
const logPath = logFlag ? resolve3(logFlag) : resolve3(dirname3(dashboardPath), "..", "public", "daily-analysis.json");
|
|
1507
3058
|
if (!existsSync3(logPath)) return null;
|
|
1508
3059
|
try {
|
|
1509
|
-
const
|
|
1510
|
-
const entries =
|
|
3060
|
+
const json2 = JSON.parse(readFileSync(logPath, "utf8"));
|
|
3061
|
+
const entries = json2.entries ?? [];
|
|
1511
3062
|
return entries.length ? entries[entries.length - 1] : null;
|
|
1512
3063
|
} catch {
|
|
1513
3064
|
return null;
|
|
@@ -1573,7 +3124,7 @@ usage:
|
|
|
1573
3124
|
frames) for the agent to fill in; --title <t>,
|
|
1574
3125
|
--author <a>, --force to overwrite
|
|
1575
3126
|
zframes serve [file] serve <dashboard.json> (default: ./dashboard.json)
|
|
1576
|
-
as a live, editable terminal at
|
|
3127
|
+
as a live, editable terminal at localhost:37263
|
|
1577
3128
|
(--port <n> to change); Save writes back to the file
|
|
1578
3129
|
zframes catalogue print the frame catalogue as JSON Schema
|
|
1579
3130
|
(this is what a generating agent reads)
|
|
@@ -1584,56 +3135,6 @@ usage:
|
|
|
1584
3135
|
stdout (the deterministic half of /zframes-brief)
|
|
1585
3136
|
zframes help this text
|
|
1586
3137
|
`;
|
|
1587
|
-
function lintSpec(spec) {
|
|
1588
|
-
const issues = [];
|
|
1589
|
-
const metaByName = new Map(frameMetas.map((meta) => [meta.name, meta]));
|
|
1590
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
1591
|
-
for (const instance of spec.frames) {
|
|
1592
|
-
if (seenIds.has(instance.id))
|
|
1593
|
-
issues.push({
|
|
1594
|
-
frameId: instance.id,
|
|
1595
|
-
message: `duplicate frame id "${instance.id}"`
|
|
1596
|
-
});
|
|
1597
|
-
seenIds.add(instance.id);
|
|
1598
|
-
const meta = metaByName.get(instance.frame);
|
|
1599
|
-
if (!meta) {
|
|
1600
|
-
issues.push({
|
|
1601
|
-
frameId: instance.id,
|
|
1602
|
-
message: `unknown frame "${instance.frame}". available: ${[
|
|
1603
|
-
...metaByName.keys()
|
|
1604
|
-
].join(", ")}`
|
|
1605
|
-
});
|
|
1606
|
-
continue;
|
|
1607
|
-
}
|
|
1608
|
-
const parsed = meta.schema.safeParse(instance.config);
|
|
1609
|
-
if (!parsed.success) {
|
|
1610
|
-
for (const issue of parsed.error.issues) {
|
|
1611
|
-
issues.push({
|
|
1612
|
-
frameId: instance.id,
|
|
1613
|
-
message: `config.${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
1614
|
-
});
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
if (instance.position.x + instance.position.w > spec.grid.columns)
|
|
1618
|
-
issues.push({
|
|
1619
|
-
frameId: instance.id,
|
|
1620
|
-
message: `overflows the grid: x(${instance.position.x}) + w(${instance.position.w}) > ${spec.grid.columns} columns`
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
for (let i = 0; i < spec.frames.length; i++) {
|
|
1624
|
-
for (let j = i + 1; j < spec.frames.length; j++) {
|
|
1625
|
-
const a = spec.frames[i].position;
|
|
1626
|
-
const b = spec.frames[j].position;
|
|
1627
|
-
const overlap = a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
1628
|
-
if (overlap)
|
|
1629
|
-
issues.push({
|
|
1630
|
-
frameId: spec.frames[i].id,
|
|
1631
|
-
message: `overlaps frame "${spec.frames[j].id}"`
|
|
1632
|
-
});
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
return issues;
|
|
1636
|
-
}
|
|
1637
3138
|
function lint(file) {
|
|
1638
3139
|
let raw;
|
|
1639
3140
|
try {
|
|
@@ -1642,14 +3143,14 @@ function lint(file) {
|
|
|
1642
3143
|
console.error(`\u2717 cannot read ${file}`);
|
|
1643
3144
|
return 1;
|
|
1644
3145
|
}
|
|
1645
|
-
let
|
|
3146
|
+
let json2;
|
|
1646
3147
|
try {
|
|
1647
|
-
|
|
3148
|
+
json2 = JSON.parse(raw);
|
|
1648
3149
|
} catch (error) {
|
|
1649
3150
|
console.error(`\u2717 ${file} is not valid JSON: ${error.message}`);
|
|
1650
3151
|
return 1;
|
|
1651
3152
|
}
|
|
1652
|
-
const parsed = DashboardSpecSchema.safeParse(
|
|
3153
|
+
const parsed = DashboardSpecSchema.safeParse(json2);
|
|
1653
3154
|
if (!parsed.success) {
|
|
1654
3155
|
console.error(`\u2717 ${file} is not a valid dashboard spec:`);
|
|
1655
3156
|
for (const issue of parsed.error.issues)
|
|
@@ -1698,7 +3199,16 @@ async function main() {
|
|
|
1698
3199
|
return 1;
|
|
1699
3200
|
}
|
|
1700
3201
|
}
|
|
1701
|
-
main().then(
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
3202
|
+
main().then(
|
|
3203
|
+
(code) => {
|
|
3204
|
+
if (process.stdout.write("")) {
|
|
3205
|
+
process.exit(code);
|
|
3206
|
+
} else {
|
|
3207
|
+
process.stdout.once("drain", () => process.exit(code));
|
|
3208
|
+
}
|
|
3209
|
+
},
|
|
3210
|
+
(error) => {
|
|
3211
|
+
console.error(error);
|
|
3212
|
+
process.exit(1);
|
|
3213
|
+
}
|
|
3214
|
+
);
|