zframes 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1385 -124
- package/package.json +2 -2
- package/runtime/assets/bitcoin-dominance-DVZsIJ3y.js +1 -0
- package/runtime/assets/breakeven-lskbHkqb.js +1 -0
- package/runtime/assets/breathing-BeUkCUAJ.js +1 -0
- package/runtime/assets/btc-blocks-fXBgGo4z.js +1 -0
- package/runtime/assets/btc-difficulty-BwyxgI0e.js +1 -0
- package/runtime/assets/btc-fees-Bg1rcXtN.js +1 -0
- package/runtime/assets/btc-hashrate-Xg8QNlTO.js +1 -0
- package/runtime/assets/btc-mempool-8Mfa77fO.js +1 -0
- package/runtime/assets/btc-shared-DeqDaCze.js +1 -0
- package/runtime/assets/calculator-CiXyJA9p.js +1 -0
- package/runtime/assets/chart-utils-BYIZMM9J.js +1 -0
- package/runtime/assets/checklist-D0dKSdmf.js +1 -0
- package/runtime/assets/clock-CNQTNh4L.js +1 -0
- package/runtime/assets/coin-movers-DulAF_oo.js +1 -0
- package/runtime/assets/content-shared-BZ1XzrJx.js +1 -0
- package/runtime/assets/countdown-BzZ2L6bw.js +1 -0
- package/runtime/assets/{d3-B3esOK0k.js → d3-xK4fOkBU.js} +1 -1
- package/runtime/assets/daily-analysis-RyI5KL4T.js +1 -0
- package/runtime/assets/day-meter-2bAj77Kz.js +1 -0
- package/runtime/assets/dex-volume-chart-DDPf1YAN.js +1 -0
- package/runtime/assets/dex-volume-treemap-jZCgRREu.js +1 -0
- package/runtime/assets/dice-wg979Skm.js +1 -0
- package/runtime/assets/dino-game-DlACWmE_.js +1 -0
- package/runtime/assets/divider-Chz1UsWJ.js +1 -0
- package/runtime/assets/drawdy-Ce7P47cx.js +1 -0
- package/runtime/assets/editor-BlGZ_tel.js +68 -0
- package/runtime/assets/editor-CHAAJRb1.css +1 -0
- package/runtime/assets/fear-greed-CENzXdIk.js +1 -0
- package/runtime/assets/feed-row-mVg1d5Zj.js +1 -0
- package/runtime/assets/filings-feed-2aIKzUUY.js +1 -0
- package/runtime/assets/financial-stress--ezsCpfR.js +1 -0
- package/runtime/assets/flappy-bird-DDe-IXt9.js +1 -0
- package/runtime/assets/format-CfAcS4D8.js +1 -0
- package/runtime/assets/fundamentals-v8f81ZNS.js +1 -0
- package/runtime/assets/funding-heatmap-DUoeFNGr.js +1 -0
- package/runtime/assets/funding-rate-chart-CjDA4nj7.js +1 -0
- package/runtime/assets/fx-board-CmXYQZi4.js +1 -0
- package/runtime/assets/game-ui-Dy_QYsPr.js +1 -0
- package/runtime/assets/heading-DMxdj9LK.js +1 -0
- package/runtime/assets/holiday-calendar-DxB-fxG5.js +1 -0
- package/runtime/assets/image-DLg33JpH.js +1 -0
- package/runtime/assets/index-C_O7gp2F.js +822 -0
- package/runtime/assets/index-DRTPEQtR.css +1 -0
- package/runtime/assets/index-XGtH5pVY.css +1 -0
- package/runtime/assets/index-jnySFpSv.js +1 -0
- package/runtime/assets/inflation-pulse-gcw-84PM.js +1 -0
- package/runtime/assets/journal-log-BX5Z_yte.js +1 -0
- package/runtime/assets/journal-open-XFQGIRkw.js +1 -0
- package/runtime/assets/journal-results-J1WCmyH2.js +1 -0
- package/runtime/assets/journal-score-CKhBSfYY.js +1 -0
- package/runtime/assets/journal-store-KCRoPnI_.js +1 -0
- package/runtime/assets/journal-ui-Cogk2Wkt.js +1 -0
- package/runtime/assets/labor-market-BV20Rl-4.js +1 -0
- package/runtime/assets/lightning-stats-h4fquhAg.js +1 -0
- package/runtime/assets/link-grid-a6NWj1Hu.js +1 -0
- package/runtime/assets/market-cap-treemap-CAi49oc_.js +1 -0
- package/runtime/assets/market-data-CvmpLFSo.js +1 -0
- package/runtime/assets/market-hours-6srwf0-r.js +1 -0
- package/runtime/assets/marquee-DDurk6bj.js +28 -0
- package/runtime/assets/metric-row-BVVhjsjK.js +1 -0
- package/runtime/assets/mini-line-chart-D-i1QqSe.js +1 -0
- package/runtime/assets/mining-pools-BPjCy4ZW.js +1 -0
- package/runtime/assets/mover-row-kQ9xt3Sp.js +1 -0
- package/runtime/assets/national-debt-DkOnmfd1.js +1 -0
- package/runtime/assets/news-feed-CFWKKqrH.js +1 -0
- package/runtime/assets/note-_cGEXAru.js +1 -0
- package/runtime/assets/open-interest-fxHh_T-V.js +1 -0
- package/runtime/assets/options-iv-CDArg1Op.js +1 -0
- package/runtime/assets/options-oi-strike-D_XIgUkk.js +1 -0
- package/runtime/assets/options-put-call-DG4RXjzD.js +1 -0
- package/runtime/assets/pomodoro-B-Jk-51G.js +1 -0
- package/runtime/assets/portfolio-allocation-BJepNj1X.js +1 -0
- package/runtime/assets/portfolio-common-D5L06PNC.js +1 -0
- package/runtime/assets/portfolio-holdings-DSkAZALG.js +1 -0
- package/runtime/assets/portfolio-value-BJw2csWV.js +1 -0
- package/runtime/assets/price-chart-khxI-9vJ.js +1 -0
- package/runtime/assets/price-compare-C-uEckke.js +1 -0
- package/runtime/assets/price-liveline-BDesEIbL.js +1 -0
- package/runtime/assets/price-ticker-BXSeNYf9.js +1 -0
- package/runtime/assets/protocol-fees-treemap-YtWbL1Kn.js +1 -0
- package/runtime/assets/protocol-tvl-chart-CEjUOIY5.js +1 -0
- package/runtime/assets/protocol-tvl-treemap-b31QlpnS.js +1 -0
- package/runtime/assets/quote-cehyepdm.js +1 -0
- package/runtime/assets/rates-board-Du4k0z1i.js +1 -0
- package/runtime/assets/returns-projector-DXnRFAES.js +1 -0
- package/runtime/assets/risk-reward-DZWyCrbv.js +1 -0
- package/runtime/assets/rules-card-BXJjNTqj.js +1 -0
- package/runtime/assets/session-progress-bA45Q3wk.js +1 -0
- package/runtime/assets/short-volume-C6cWknyR.js +1 -0
- package/runtime/assets/snake-DYTa97hl.js +1 -0
- package/runtime/assets/spotify-embed-BaH6UHeG.js +1 -0
- package/runtime/assets/stopwatch-CS6n0bY9.js +1 -0
- package/runtime/assets/top-movers-CQNx9EH5.js +1 -0
- package/runtime/assets/treasury-auctions-CGM8hAuU.js +1 -0
- package/runtime/assets/treemap-leaf-DLQawrx4.js +1 -0
- package/runtime/assets/tvl-treemap-C3SUFzdj.js +1 -0
- package/runtime/assets/ui-Bq3RQsdJ.js +1 -0
- package/runtime/assets/unicorn-scene-DcohHRPZ.js +1 -0
- package/runtime/assets/use-countdown-BmcF-qdL.js +1 -0
- package/runtime/assets/utils-DaT-yT0k.js +1 -0
- package/runtime/assets/video-WCnvLwQ_.js +1 -0
- package/runtime/assets/yield-curve-CpitTQ9L.js +1 -0
- package/runtime/index.html +2 -4
- 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/editor-CY2XpG9x.js +0 -68
- package/runtime/assets/editor-DinyMxo6.css +0 -1
- package/runtime/assets/index-Bpmi3x1d.js +0 -597
- package/runtime/assets/index-CsheOEUr.css +0 -1
- package/runtime/assets/index-DPlG_EDx.js +0 -35727
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
5
5
|
|
|
6
6
|
// ../core/src/catalogue.ts
|
|
7
7
|
import { z } from "zod";
|
|
@@ -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,13 +58,24 @@ 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
|
)
|
|
39
67
|
});
|
|
40
68
|
var BackgroundSchema = z2.object({
|
|
41
|
-
type: z2.enum(["none", "gradient", "unicorn"]).default("
|
|
42
|
-
"Background style: '
|
|
69
|
+
type: z2.enum(["none", "color", "gradient", "unicorn"]).default("none").describe(
|
|
70
|
+
"Background style: 'none' (the signature dark indigo glow \u2014 the default), 'color' (a solid colour), 'gradient' (a custom two-colour linear gradient), or 'unicorn' (an animated Unicorn Studio scene)."
|
|
71
|
+
),
|
|
72
|
+
color: z2.string().default("#0a0a12").describe(
|
|
73
|
+
"Solid background colour (any CSS colour). Used when type is 'color'."
|
|
74
|
+
),
|
|
75
|
+
gradientFrom: z2.string().default("#1b1e4d").describe("Gradient start colour. Used when type is 'gradient'."),
|
|
76
|
+
gradientTo: z2.string().default("#07070c").describe("Gradient end colour. Used when type is 'gradient'."),
|
|
77
|
+
gradientAngle: z2.number().min(0).max(360).default(160).describe(
|
|
78
|
+
"Gradient angle in degrees (0 = upward, 90 = rightward, 180 = downward). Used when type is 'gradient'."
|
|
43
79
|
),
|
|
44
80
|
projectId: z2.string().optional().describe(
|
|
45
81
|
"Unicorn Studio public project id \u2014 required when type is 'unicorn'."
|
|
@@ -56,8 +92,35 @@ var ThemeSchema = z2.object({
|
|
|
56
92
|
),
|
|
57
93
|
accentSat: z2.number().min(0).max(100).default(90).describe(
|
|
58
94
|
"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."
|
|
95
|
+
),
|
|
96
|
+
baseHue: z2.number().int().min(0).max(360).default(233).describe(
|
|
97
|
+
"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."
|
|
98
|
+
),
|
|
99
|
+
baseSat: z2.number().min(0).max(100).default(20).describe(
|
|
100
|
+
"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."
|
|
101
|
+
),
|
|
102
|
+
upColor: z2.string().regex(/^#[0-9a-fA-F]{6}$/, "must be a 6-digit hex colour like #3fd08f").default("#3fd08f").describe(
|
|
103
|
+
"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."
|
|
104
|
+
),
|
|
105
|
+
downColor: z2.string().regex(/^#[0-9a-fA-F]{6}$/, "must be a 6-digit hex colour like #ff6b81").default("#ff6b81").describe(
|
|
106
|
+
"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
107
|
)
|
|
60
|
-
}).describe(
|
|
108
|
+
}).describe(
|
|
109
|
+
"Dashboard-wide colour identity \u2014 accent (rims/highlights), the dark card-surface tint (base), and the semantic up/down (gain/loss) colours."
|
|
110
|
+
);
|
|
111
|
+
var TypographySchema = z2.object({
|
|
112
|
+
fontFamily: z2.enum(["sans", "mono", "serif"]).default("sans").describe(
|
|
113
|
+
"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."
|
|
114
|
+
),
|
|
115
|
+
numericStyle: z2.enum(["proportional", "tabular"]).default("proportional").describe(
|
|
116
|
+
"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."
|
|
117
|
+
),
|
|
118
|
+
scale: z2.number().min(0.85).max(1.25).default(1).describe(
|
|
119
|
+
"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."
|
|
120
|
+
)
|
|
121
|
+
}).describe(
|
|
122
|
+
"Dashboard-wide typography \u2014 font family, numeric digit style, and global text scale."
|
|
123
|
+
);
|
|
61
124
|
var AppearanceSchema = z2.object({
|
|
62
125
|
radius: z2.number().min(0).default(18).describe(
|
|
63
126
|
"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."
|
|
@@ -80,11 +143,11 @@ var AppearanceSchema = z2.object({
|
|
|
80
143
|
var DashboardSpecSchema = z2.preprocess(
|
|
81
144
|
(raw) => {
|
|
82
145
|
if (!raw || typeof raw !== "object") return raw;
|
|
83
|
-
|
|
146
|
+
let r = raw;
|
|
84
147
|
const grid = r.grid;
|
|
85
148
|
if (grid && typeof grid === "object" && "radius" in grid) {
|
|
86
149
|
const { radius, ...restGrid } = grid;
|
|
87
|
-
|
|
150
|
+
r = {
|
|
88
151
|
...r,
|
|
89
152
|
grid: restGrid,
|
|
90
153
|
appearance: {
|
|
@@ -93,7 +156,14 @@ var DashboardSpecSchema = z2.preprocess(
|
|
|
93
156
|
}
|
|
94
157
|
};
|
|
95
158
|
}
|
|
96
|
-
|
|
159
|
+
const bg = r.background;
|
|
160
|
+
if (bg && typeof bg === "object" && bg.type === "gradient" && !("gradientFrom" in bg)) {
|
|
161
|
+
r = {
|
|
162
|
+
...r,
|
|
163
|
+
background: { ...bg, type: "none" }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return r;
|
|
97
167
|
},
|
|
98
168
|
z2.object({
|
|
99
169
|
version: z2.coerce.string().default("1.0.0").describe(
|
|
@@ -104,19 +174,51 @@ var DashboardSpecSchema = z2.preprocess(
|
|
|
104
174
|
`Who made this dashboard \u2014 a free-form credit, like package.json's author ("You" or "You <you@example.com>"). Optional.`
|
|
105
175
|
),
|
|
106
176
|
grid: z2.object({
|
|
107
|
-
|
|
108
|
-
|
|
177
|
+
mode: z2.enum(["flow-vertical", "flow-horizontal", "canvas"]).default("flow-vertical").describe(
|
|
178
|
+
"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)."
|
|
179
|
+
),
|
|
180
|
+
columns: z2.number().int().min(1).default(12).describe(
|
|
181
|
+
"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."
|
|
182
|
+
),
|
|
183
|
+
rowHeight: z2.number().min(8).default(96).describe(
|
|
184
|
+
"Pixel height of each grid row (flow-vertical). A frame's `position` y/h are in these row units."
|
|
185
|
+
),
|
|
186
|
+
rows: z2.number().int().min(1).default(6).describe(
|
|
187
|
+
'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.'
|
|
188
|
+
),
|
|
109
189
|
gap: z2.number().min(0).default(12).describe(
|
|
110
190
|
"Pixels of space between frames (the grid gutter). 0 makes the cards flush; the editor's Layout rail exposes this as a slider."
|
|
111
191
|
)
|
|
112
|
-
}).default({
|
|
192
|
+
}).default({
|
|
193
|
+
mode: "flow-vertical",
|
|
194
|
+
columns: 12,
|
|
195
|
+
rowHeight: 96,
|
|
196
|
+
rows: 6,
|
|
197
|
+
gap: 12
|
|
198
|
+
}),
|
|
113
199
|
background: BackgroundSchema.default({
|
|
114
|
-
type: "
|
|
200
|
+
type: "none",
|
|
201
|
+
color: "#0a0a12",
|
|
202
|
+
gradientFrom: "#1b1e4d",
|
|
203
|
+
gradientTo: "#07070c",
|
|
204
|
+
gradientAngle: 160,
|
|
115
205
|
scale: 1,
|
|
116
206
|
dpi: 1.5,
|
|
117
207
|
opacity: 0.16
|
|
118
208
|
}),
|
|
119
|
-
theme: ThemeSchema.default({
|
|
209
|
+
theme: ThemeSchema.default({
|
|
210
|
+
accentHue: 242,
|
|
211
|
+
accentSat: 90,
|
|
212
|
+
baseHue: 233,
|
|
213
|
+
baseSat: 20,
|
|
214
|
+
upColor: "#3fd08f",
|
|
215
|
+
downColor: "#ff6b81"
|
|
216
|
+
}),
|
|
217
|
+
typography: TypographySchema.default({
|
|
218
|
+
fontFamily: "sans",
|
|
219
|
+
numericStyle: "proportional",
|
|
220
|
+
scale: 1
|
|
221
|
+
}),
|
|
120
222
|
appearance: AppearanceSchema.default({
|
|
121
223
|
radius: 18,
|
|
122
224
|
borderStrength: 0.22,
|
|
@@ -128,6 +230,164 @@ var DashboardSpecSchema = z2.preprocess(
|
|
|
128
230
|
})
|
|
129
231
|
);
|
|
130
232
|
|
|
233
|
+
// ../core/src/store.ts
|
|
234
|
+
import {
|
|
235
|
+
mkdirSync,
|
|
236
|
+
readdirSync,
|
|
237
|
+
readFileSync,
|
|
238
|
+
statSync,
|
|
239
|
+
writeFileSync
|
|
240
|
+
} from "fs";
|
|
241
|
+
import { homedir } from "os";
|
|
242
|
+
import { join, resolve } from "path";
|
|
243
|
+
var NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
244
|
+
function isValidName(name) {
|
|
245
|
+
return name.length <= 64 && NAME_RE.test(name);
|
|
246
|
+
}
|
|
247
|
+
function storeHome() {
|
|
248
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
249
|
+
const base = xdg && xdg.trim() ? xdg.trim() : join(homedir(), ".config");
|
|
250
|
+
return join(base, "zframes");
|
|
251
|
+
}
|
|
252
|
+
function dashboardsDir() {
|
|
253
|
+
return join(storeHome(), "dashboards");
|
|
254
|
+
}
|
|
255
|
+
function configPath() {
|
|
256
|
+
return join(storeHome(), "config.json");
|
|
257
|
+
}
|
|
258
|
+
function credentialsFile() {
|
|
259
|
+
return join(storeHome(), "credentials.json");
|
|
260
|
+
}
|
|
261
|
+
function dashboardPath(name) {
|
|
262
|
+
return join(dashboardsDir(), name, "dashboard.json");
|
|
263
|
+
}
|
|
264
|
+
function findDashboardFile(name) {
|
|
265
|
+
const folder = dashboardPath(name);
|
|
266
|
+
if (isFile(folder)) return folder;
|
|
267
|
+
const flat = join(dashboardsDir(), `${name}.json`);
|
|
268
|
+
if (isFile(flat)) return flat;
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
function ensureHome() {
|
|
272
|
+
mkdirSync(storeHome(), { recursive: true, mode: 448 });
|
|
273
|
+
mkdirSync(dashboardsDir(), { recursive: true });
|
|
274
|
+
}
|
|
275
|
+
function isFile(file) {
|
|
276
|
+
try {
|
|
277
|
+
return statSync(file).isFile();
|
|
278
|
+
} catch {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function expandTilde(p) {
|
|
283
|
+
if (p === "~") return homedir();
|
|
284
|
+
if (p.startsWith("~/")) return join(homedir(), p.slice(2));
|
|
285
|
+
return p;
|
|
286
|
+
}
|
|
287
|
+
function looksLikePath(arg) {
|
|
288
|
+
return arg.includes("/") || arg.includes("\\") || arg.startsWith(".") || arg.startsWith("~") || arg.toLowerCase().endsWith(".json");
|
|
289
|
+
}
|
|
290
|
+
function classifyTarget(arg, cwd) {
|
|
291
|
+
if (looksLikePath(arg)) {
|
|
292
|
+
return { kind: "path", file: resolve(cwd, expandTilde(arg)) };
|
|
293
|
+
}
|
|
294
|
+
if (!isValidName(arg)) {
|
|
295
|
+
return {
|
|
296
|
+
error: `"${arg}" is not a valid dashboard name (lowercase letters, digits, "-", "_") or a path to a .json file`
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return { kind: "store", name: arg, file: dashboardPath(arg) };
|
|
300
|
+
}
|
|
301
|
+
function readConfig() {
|
|
302
|
+
try {
|
|
303
|
+
const json2 = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
304
|
+
return json2 && typeof json2 === "object" ? json2 : {};
|
|
305
|
+
} catch {
|
|
306
|
+
return {};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function getDefault() {
|
|
310
|
+
const d = readConfig().default;
|
|
311
|
+
return typeof d === "string" && d ? d : null;
|
|
312
|
+
}
|
|
313
|
+
function setDefault(name) {
|
|
314
|
+
ensureHome();
|
|
315
|
+
const cfg = readConfig();
|
|
316
|
+
cfg.default = name;
|
|
317
|
+
writeFileSync(configPath(), `${JSON.stringify(cfg, null, 2)}
|
|
318
|
+
`, "utf8");
|
|
319
|
+
}
|
|
320
|
+
function listDashboards() {
|
|
321
|
+
const dir = dashboardsDir();
|
|
322
|
+
const names = /* @__PURE__ */ new Set();
|
|
323
|
+
try {
|
|
324
|
+
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
|
325
|
+
if (ent.isDirectory()) {
|
|
326
|
+
if (isValidName(ent.name) && isFile(join(dir, ent.name, "dashboard.json")))
|
|
327
|
+
names.add(ent.name);
|
|
328
|
+
} else if (ent.isFile() && ent.name.toLowerCase().endsWith(".json")) {
|
|
329
|
+
const stem = ent.name.slice(0, -5);
|
|
330
|
+
if (isValidName(stem)) names.add(stem);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
const def = getDefault();
|
|
337
|
+
return [...names].sort().map((name) => {
|
|
338
|
+
const file = findDashboardFile(name) ?? dashboardPath(name);
|
|
339
|
+
let title = null;
|
|
340
|
+
try {
|
|
341
|
+
const json2 = JSON.parse(readFileSync(file, "utf8"));
|
|
342
|
+
if (typeof json2.title === "string") title = json2.title;
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
return { name, file, title, isDefault: name === def };
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function resolveServeTarget(arg, cwd) {
|
|
349
|
+
if (arg !== void 0) {
|
|
350
|
+
const c = classifyTarget(arg, cwd);
|
|
351
|
+
if ("error" in c) return c;
|
|
352
|
+
if (c.kind === "store") {
|
|
353
|
+
const file = findDashboardFile(c.name);
|
|
354
|
+
if (!file) {
|
|
355
|
+
return {
|
|
356
|
+
error: `no dashboard named "${c.name}" in the store (${dashboardsDir()})
|
|
357
|
+
create it with \`zframes init ${c.name}\`, or run \`zframes list\` to see what's there.`
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return { kind: "store", name: c.name, file };
|
|
361
|
+
}
|
|
362
|
+
if (!isFile(c.file)) {
|
|
363
|
+
return {
|
|
364
|
+
error: `no dashboard.json at ${arg}
|
|
365
|
+
pass a path, a store name, or run from a directory that has one.`
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return c;
|
|
369
|
+
}
|
|
370
|
+
const def = getDefault();
|
|
371
|
+
if (def) {
|
|
372
|
+
const file = findDashboardFile(def);
|
|
373
|
+
if (file) return { kind: "store", name: def, file };
|
|
374
|
+
}
|
|
375
|
+
const cwdFile = join(cwd, "dashboard.json");
|
|
376
|
+
if (isFile(cwdFile)) return { kind: "path", file: cwdFile };
|
|
377
|
+
const entries = listDashboards();
|
|
378
|
+
if (entries.length === 1) {
|
|
379
|
+
return { kind: "store", name: entries[0].name, file: entries[0].file };
|
|
380
|
+
}
|
|
381
|
+
if (entries.length > 1) {
|
|
382
|
+
return {
|
|
383
|
+
error: `no default dashboard set, and ${entries.length} in the store \u2014 pass a name (\`zframes serve <name>\`), set a default (\`zframes use <name>\`), or list them (\`zframes list\`).`
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
error: "no dashboard found \u2014 create one with `zframes init <name>`, or pass a path to a dashboard.json."
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
131
391
|
// ../core/src/frame.ts
|
|
132
392
|
function defineFrameMeta(meta) {
|
|
133
393
|
return meta;
|
|
@@ -161,10 +421,12 @@ var SOURCES = {
|
|
|
161
421
|
},
|
|
162
422
|
mempool: { name: "mempool.space", url: "https://mempool.space" },
|
|
163
423
|
deribit: { name: "Deribit", url: "https://www.deribit.com" },
|
|
164
|
-
coinpaprika: { name: "Coinpaprika", url: "https://coinpaprika.com" }
|
|
424
|
+
coinpaprika: { name: "Coinpaprika", url: "https://coinpaprika.com" },
|
|
425
|
+
frankfurter: { name: "Frankfurter (ECB)", url: "https://frankfurter.dev" }
|
|
165
426
|
};
|
|
166
427
|
var clockMeta = defineFrameMeta({
|
|
167
428
|
name: "clock",
|
|
429
|
+
category: "tools",
|
|
168
430
|
iconUrl: widgetIcon("clock"),
|
|
169
431
|
layout: { w: 3, h: 2, minW: 2, minH: 1 },
|
|
170
432
|
description: "Digital clock showing the current time, ticking every second. Configurable IANA timezone (defaults to the viewer's local zone), 12/24-hour format, optional seconds and date, the timezone abbreviation, and a caption label. Drop several with different timezones for a trading-desk world clock. Needs no data provider.",
|
|
@@ -189,6 +451,7 @@ var clockMeta = defineFrameMeta({
|
|
|
189
451
|
});
|
|
190
452
|
var marketHoursMeta = defineFrameMeta({
|
|
191
453
|
name: "market-hours",
|
|
454
|
+
category: "tools",
|
|
192
455
|
iconUrl: widgetIcon("market-hours"),
|
|
193
456
|
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
194
457
|
description: "Which world stock exchanges are open right now \u2014 each row shows an open / closed / holiday status dot and a live countdown to the next open or close. Computed entirely client-side from each exchange's timezone and regular trading hours (no API); a bundled 2026 holiday list keeps the major Western exchanges accurate on market holidays. Intraday lunch breaks and half-day early closes are not modelled. Needs no data provider.",
|
|
@@ -204,6 +467,7 @@ var marketHoursMeta = defineFrameMeta({
|
|
|
204
467
|
});
|
|
205
468
|
var fearGreedMeta = defineFrameMeta({
|
|
206
469
|
name: "fear-greed",
|
|
470
|
+
category: "sentiment",
|
|
207
471
|
iconUrl: widgetIcon("fear-greed"),
|
|
208
472
|
layout: { w: 3, h: 3, minW: 2, minH: 2 },
|
|
209
473
|
description: "Crypto Fear & Greed index (0 = extreme fear, 100 = extreme greed) with a recent-history sparkline. A one-number market mood gauge from alternative.me.",
|
|
@@ -215,6 +479,7 @@ var fearGreedMeta = defineFrameMeta({
|
|
|
215
479
|
});
|
|
216
480
|
var fundingRateChartMeta = defineFrameMeta({
|
|
217
481
|
name: "funding-rate-chart",
|
|
482
|
+
category: "derivatives",
|
|
218
483
|
iconUrl: widgetIcon("funding-rate-chart"),
|
|
219
484
|
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
220
485
|
description: "Multi-series line chart comparing hourly perp funding rates across symbols over a configurable lookback window. Positive funding = longs pay shorts. Useful for spotting crowded trades.",
|
|
@@ -229,6 +494,7 @@ var fundingRateChartMeta = defineFrameMeta({
|
|
|
229
494
|
});
|
|
230
495
|
var noteMeta = defineFrameMeta({
|
|
231
496
|
name: "note",
|
|
497
|
+
category: "layout",
|
|
232
498
|
iconUrl: widgetIcon("note"),
|
|
233
499
|
layout: { w: 4, h: 3, minW: 2, minH: 2 },
|
|
234
500
|
description: "Free-form text note pinned to the dashboard \u2014 trading plans, reminders, watch levels. Needs no data provider.",
|
|
@@ -240,14 +506,15 @@ var noteMeta = defineFrameMeta({
|
|
|
240
506
|
});
|
|
241
507
|
var priceChartMeta = defineFrameMeta({
|
|
242
508
|
name: "price-chart",
|
|
509
|
+
category: "markets",
|
|
243
510
|
iconUrl: widgetIcon("price-chart"),
|
|
244
511
|
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
245
|
-
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
|
|
512
|
+
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 any HIP-3 perp \u2014 stocks (xyz:TSLA), indices (xyz:SP500), commodities (xyz:GOLD) \u2014 and crypto (BTC). The centerpiece frame.",
|
|
246
513
|
capabilities: ["ohlcv", "quote-stream"],
|
|
247
514
|
source: SOURCES.hyperliquid,
|
|
248
515
|
schema: z3.object({
|
|
249
516
|
symbol: z3.string().min(1).describe(
|
|
250
|
-
'Hyperliquid symbol to chart.
|
|
517
|
+
'Hyperliquid symbol to chart. HIP-3 cross-asset: stocks "xyz:TSLA"/"xyz:NVDA", indices "xyz:SP500"/"xyz:XYZ100", commodities "xyz:GOLD"/"xyz:CL", FX "xyz:EUR". Crypto: "BTC", "ETH".'
|
|
251
518
|
),
|
|
252
519
|
interval: z3.enum(["1m", "5m", "15m", "1h", "4h", "1d"]).default("1h").describe("Candle interval."),
|
|
253
520
|
mode: z3.enum(["candle", "line"]).default("candle").describe("Candlestick or smooth line rendering."),
|
|
@@ -256,6 +523,7 @@ var priceChartMeta = defineFrameMeta({
|
|
|
256
523
|
});
|
|
257
524
|
var priceLivelineMeta = defineFrameMeta({
|
|
258
525
|
name: "price-liveline",
|
|
526
|
+
category: "markets",
|
|
259
527
|
iconUrl: widgetIcon("price-liveline"),
|
|
260
528
|
layout: { w: 6, h: 3, minW: 4, minH: 2 },
|
|
261
529
|
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.",
|
|
@@ -263,7 +531,7 @@ var priceLivelineMeta = defineFrameMeta({
|
|
|
263
531
|
source: SOURCES.hyperliquid,
|
|
264
532
|
schema: z3.object({
|
|
265
533
|
symbols: z3.array(z3.string()).min(2).max(8).describe(
|
|
266
|
-
'Hyperliquid symbols to stream together, e.g. ["xyz:TSLA", "xyz:NVDA", "xyz:AAPL"] or ["BTC", "ETH", "SOL"]. 2 to 8.'
|
|
534
|
+
'Hyperliquid symbols to stream together \u2014 great cross-asset (the normalized view races a stock vs the index vs gold vs crude), e.g. ["xyz:NVDA", "xyz:SP500", "xyz:GOLD", "xyz:CL"], or all-equity ["xyz:TSLA", "xyz:NVDA", "xyz:AAPL"], or crypto ["BTC", "ETH", "SOL"]. 2 to 8.'
|
|
267
535
|
),
|
|
268
536
|
windowSec: z3.number().int().min(10).max(300).default(30).describe(
|
|
269
537
|
"Rolling live window in seconds. 30 mirrors the zhive liveline view; use 60\u2013300 for slower dashboards."
|
|
@@ -275,6 +543,7 @@ var priceLivelineMeta = defineFrameMeta({
|
|
|
275
543
|
});
|
|
276
544
|
var priceTickerMeta = defineFrameMeta({
|
|
277
545
|
name: "price-ticker",
|
|
546
|
+
category: "markets",
|
|
278
547
|
iconUrl: widgetIcon("price-ticker"),
|
|
279
548
|
layout: { w: 3, h: 3, minW: 2, minH: 2 },
|
|
280
549
|
description: "Live watchlist streaming mid prices over the Hyperliquid WebSocket with 24h change per symbol. The bread-and-butter frame for any dashboard.",
|
|
@@ -282,12 +551,13 @@ var priceTickerMeta = defineFrameMeta({
|
|
|
282
551
|
source: SOURCES.hyperliquid,
|
|
283
552
|
schema: z3.object({
|
|
284
553
|
symbols: z3.array(z3.string()).min(1).describe(
|
|
285
|
-
'Hyperliquid symbols to track, e.g. ["xyz:TSLA", "xyz:NVDA", "xyz:AAPL"]. Crypto works too: "BTC", "ETH".'
|
|
554
|
+
'Hyperliquid symbols to track \u2014 mix asset classes, e.g. ["xyz:NVDA", "xyz:SP500", "xyz:GOLD", "xyz:EUR"] or all-equity ["xyz:TSLA", "xyz:NVDA", "xyz:AAPL"]. Crypto works too: "BTC", "ETH".'
|
|
286
555
|
)
|
|
287
556
|
})
|
|
288
557
|
});
|
|
289
558
|
var topMoversMeta = defineFrameMeta({
|
|
290
559
|
name: "top-movers",
|
|
560
|
+
category: "markets",
|
|
291
561
|
iconUrl: widgetIcon("top-movers"),
|
|
292
562
|
layout: { w: 5, h: 3, minW: 3, minH: 3 },
|
|
293
563
|
description: "Today's biggest stock and commodity HIP-3 gainers and losers (no bare crypto), side by side with current price and 24h change.",
|
|
@@ -299,6 +569,7 @@ var topMoversMeta = defineFrameMeta({
|
|
|
299
569
|
});
|
|
300
570
|
var tvlTreemapMeta = defineFrameMeta({
|
|
301
571
|
name: "tvl-treemap",
|
|
572
|
+
category: "crypto",
|
|
302
573
|
iconUrl: widgetIcon("tvl-treemap"),
|
|
303
574
|
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
304
575
|
description: "Treemap of total value locked (TVL) across the largest blockchain ecosystems, sized by TVL. Data from DeFiLlama. Good single-glance answer to 'where does on-chain capital live right now'.",
|
|
@@ -310,6 +581,7 @@ var tvlTreemapMeta = defineFrameMeta({
|
|
|
310
581
|
});
|
|
311
582
|
var bitcoinDominanceMeta = defineFrameMeta({
|
|
312
583
|
name: "bitcoin-dominance",
|
|
584
|
+
category: "crypto",
|
|
313
585
|
iconUrl: widgetIcon("bitcoin-dominance"),
|
|
314
586
|
layout: { w: 4, h: 2, minW: 3, minH: 2 },
|
|
315
587
|
description: "BTC / ETH / Others market-cap dominance as a segmented bar, with optional total marketcap line. Shifts in BTC dominance hint at where the market rotates next.",
|
|
@@ -323,6 +595,7 @@ var bitcoinDominanceMeta = defineFrameMeta({
|
|
|
323
595
|
});
|
|
324
596
|
var ratesBoardMeta = defineFrameMeta({
|
|
325
597
|
name: "rates-board",
|
|
598
|
+
category: "macro",
|
|
326
599
|
iconUrl: widgetIcon("rates-board"),
|
|
327
600
|
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
328
601
|
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.",
|
|
@@ -336,8 +609,27 @@ var ratesBoardMeta = defineFrameMeta({
|
|
|
336
609
|
maxTreasuryRates: z3.number().int().min(1).max(8).default(4).describe("How many Treasury average-rate rows to show.")
|
|
337
610
|
})
|
|
338
611
|
});
|
|
612
|
+
var fxBoardMeta = defineFrameMeta({
|
|
613
|
+
name: "fx-board",
|
|
614
|
+
category: "macro",
|
|
615
|
+
iconUrl: widgetIcon("fx-board"),
|
|
616
|
+
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
617
|
+
description: "Foreign-exchange board from the ECB's free daily reference rates (via Frankfurter, no key): each currency's latest rate vs a base, its day-over-day change, and a short trend sparkline. Daily reference data with broader currency coverage than the handful of FX perps \u2014 not a live intraday quote feed.",
|
|
618
|
+
capabilities: ["fx-rates"],
|
|
619
|
+
source: SOURCES.frankfurter,
|
|
620
|
+
schema: z3.object({
|
|
621
|
+
base: z3.string().length(3).default("USD").describe(
|
|
622
|
+
'Base currency (ISO 4217 code) each rate is quoted against, e.g. "USD". One unit of the base buys the shown amount of each listed currency.'
|
|
623
|
+
),
|
|
624
|
+
symbols: z3.array(z3.string().length(3)).min(1).max(12).default(["EUR", "GBP", "JPY", "CHF", "CAD", "AUD"]).describe(
|
|
625
|
+
'Currencies to show (ISO 4217 codes), e.g. ["EUR","GBP","JPY"]. The ECB publishes ~30; a code equal to the base is skipped.'
|
|
626
|
+
),
|
|
627
|
+
showSparkline: z3.boolean().default(true).describe("Show a small ~30-day trend sparkline next to each rate.")
|
|
628
|
+
})
|
|
629
|
+
});
|
|
339
630
|
var inflationPulseMeta = defineFrameMeta({
|
|
340
631
|
name: "inflation-pulse",
|
|
632
|
+
category: "macro",
|
|
341
633
|
iconUrl: widgetIcon("inflation-pulse"),
|
|
342
634
|
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
343
635
|
description: "BLS CPI pulse from the public no-key API: latest CPI-U all-items index with month-over-month and year-over-year changes plus a small trend sparkline. Monthly macro context for stock dashboards; not a live price feed.",
|
|
@@ -349,6 +641,7 @@ var inflationPulseMeta = defineFrameMeta({
|
|
|
349
641
|
});
|
|
350
642
|
var financialStressMeta = defineFrameMeta({
|
|
351
643
|
name: "financial-stress",
|
|
644
|
+
category: "macro",
|
|
352
645
|
iconUrl: widgetIcon("financial-stress"),
|
|
353
646
|
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
354
647
|
description: "The OFR Financial Stress Index \u2014 a daily, market-based gauge of systemic financial stress from the U.S. Office of Financial Research. One headline value where 0 is the long-run average (positive = elevated stress, negative = calmer than normal), an optional breakdown of the five contributing categories (credit, equity valuation, safe assets, funding, volatility), and a trend line. Keyless official data, updated each business day; needs the zframes runtime's data proxy (ships with `zframes serve` / `vite dev`). Not a price feed.",
|
|
@@ -363,6 +656,7 @@ var financialStressMeta = defineFrameMeta({
|
|
|
363
656
|
});
|
|
364
657
|
var nationalDebtMeta = defineFrameMeta({
|
|
365
658
|
name: "national-debt",
|
|
659
|
+
category: "macro",
|
|
366
660
|
iconUrl: widgetIcon("national-debt"),
|
|
367
661
|
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
368
662
|
description: "U.S. total public debt outstanding from the Treasury's keyless 'Debt to the Penny' dataset \u2014 the headline total in trillions, the change over the chosen window, an optional split into debt held by the public vs intragovernmental holdings, and a trend line. Official data updated each business day; CORS-safe (no proxy needed). Macro context, not a live price feed.",
|
|
@@ -379,6 +673,7 @@ var nationalDebtMeta = defineFrameMeta({
|
|
|
379
673
|
});
|
|
380
674
|
var laborMarketMeta = defineFrameMeta({
|
|
381
675
|
name: "labor-market",
|
|
676
|
+
category: "macro",
|
|
382
677
|
iconUrl: widgetIcon("labor-market"),
|
|
383
678
|
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
384
679
|
description: "U.S. labor-market snapshot from the BLS keyless public API: the headline unemployment rate, the latest monthly change in nonfarm payrolls (jobs added or lost), the total payroll level, and an unemployment-rate trend line. Monthly macro context for stock dashboards; updates on the BLS jobs-report schedule, not a live feed.",
|
|
@@ -390,6 +685,7 @@ var laborMarketMeta = defineFrameMeta({
|
|
|
390
685
|
});
|
|
391
686
|
var treasuryAuctionsMeta = defineFrameMeta({
|
|
392
687
|
name: "treasury-auctions",
|
|
688
|
+
category: "macro",
|
|
393
689
|
iconUrl: widgetIcon("treasury-auctions"),
|
|
394
690
|
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
395
691
|
description: "Recent completed U.S. Treasury auctions from the keyless Fiscal Data API \u2014 each row shows the security (bill/note/bond + term), the high awarded yield, and the bid-to-cover ratio (demand: total bids \xF7 amount accepted; higher = stronger). Newest first. Official market-plumbing data, CORS-safe; updates as auctions settle, not a live price feed.",
|
|
@@ -401,6 +697,7 @@ var treasuryAuctionsMeta = defineFrameMeta({
|
|
|
401
697
|
});
|
|
402
698
|
var filingsFeedMeta = defineFrameMeta({
|
|
403
699
|
name: "filings-feed",
|
|
700
|
+
category: "equities",
|
|
404
701
|
iconUrl: widgetIcon("filings-feed"),
|
|
405
702
|
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
406
703
|
description: "Recent SEC EDGAR filings for one US-listed company \u2014 each row shows the form type (10-K, 10-Q, 8-K, Form 4\u2026), a plain-English label, the filing date, and a click-through to the document on sec.gov, under a header with the company name, exchange, and filer category. Official data from SEC's free, CORS-safe submissions endpoint; event-driven (updates when the company files), not a price feed. Resolve by ticker (a bundled snapshot of the ~500 largest US issuers) or by raw SEC CIK for anything else.",
|
|
@@ -418,6 +715,7 @@ var filingsFeedMeta = defineFrameMeta({
|
|
|
418
715
|
});
|
|
419
716
|
var yieldCurveMeta = defineFrameMeta({
|
|
420
717
|
name: "yield-curve",
|
|
718
|
+
category: "macro",
|
|
421
719
|
iconUrl: widgetIcon("yield-curve"),
|
|
422
720
|
layout: { w: 4, h: 3, minW: 3, minH: 3 },
|
|
423
721
|
description: "The U.S. Treasury daily par yield curve \u2014 a line from 1-month to 30-year yields, the headline 2s10s spread (10Y minus 2Y; negative = inverted, the classic recession signal), and a configurable row of key maturities. Keyless official data from the U.S. Treasury, updated each business day; not a live intraday feed.",
|
|
@@ -447,6 +745,7 @@ var yieldCurveMeta = defineFrameMeta({
|
|
|
447
745
|
});
|
|
448
746
|
var fundamentalsMeta = defineFrameMeta({
|
|
449
747
|
name: "fundamentals",
|
|
748
|
+
category: "equities",
|
|
450
749
|
iconUrl: widgetIcon("fundamentals"),
|
|
451
750
|
layout: { w: 4, h: 3, minW: 3, minH: 3 },
|
|
452
751
|
description: "Headline financials for one US-listed company from SEC EDGAR XBRL company facts \u2014 revenue, net income, total assets, shareholders' equity, diluted EPS, and shares outstanding, each labelled with its fiscal period. Income-statement figures are the latest full fiscal year; balance-sheet figures are the latest reported quarter. Keyless official data that updates only when the company files (annual/quarterly), not a live feed. Requires the zframes runtime's data proxy (it ships with `zframes serve` / `vite dev`); resolve by ticker (bundled top-500 map) or raw SEC CIK.",
|
|
@@ -460,6 +759,7 @@ var fundamentalsMeta = defineFrameMeta({
|
|
|
460
759
|
});
|
|
461
760
|
var shortVolumeMeta = defineFrameMeta({
|
|
462
761
|
name: "short-volume",
|
|
762
|
+
category: "equities",
|
|
463
763
|
iconUrl: widgetIcon("short-volume"),
|
|
464
764
|
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
465
765
|
description: "Daily reported short-sale volume for a watchlist of US-listed stocks, from FINRA's free consolidated file \u2014 each row shows the % of the day's reported volume that was sold short, with a bar and the raw short/total share counts. IMPORTANT: this is reported short volume (sell-side short flow, which includes market-maker hedging), NOT short interest (outstanding short positions), and is not a directional signal on its own. Daily data published the next business day; not a live feed. US equities only.",
|
|
@@ -476,6 +776,7 @@ var shortVolumeMeta = defineFrameMeta({
|
|
|
476
776
|
});
|
|
477
777
|
var fundingHeatmapMeta = defineFrameMeta({
|
|
478
778
|
name: "funding-heatmap",
|
|
779
|
+
category: "derivatives",
|
|
479
780
|
iconUrl: widgetIcon("funding-heatmap"),
|
|
480
781
|
layout: { w: 6, h: 3, minW: 4, minH: 3 },
|
|
481
782
|
description: "Heatmap of perp funding rates \u2014 symbols as rows, daily average over the last 7 days as columns, green positive / red negative. Spots persistent funding regimes at a glance.",
|
|
@@ -489,6 +790,7 @@ var fundingHeatmapMeta = defineFrameMeta({
|
|
|
489
790
|
});
|
|
490
791
|
var dinoGameMeta = defineFrameMeta({
|
|
491
792
|
name: "dino-game",
|
|
793
|
+
category: "games",
|
|
492
794
|
iconUrl: widgetIcon("dino-game"),
|
|
493
795
|
layout: { w: 4, h: 3, minW: 3, minH: 3 },
|
|
494
796
|
description: "Chrome-dino style runner game on canvas \u2014 jump cacti with SPACE or tap. High score persists locally. For when the market is boring. Needs no data provider.",
|
|
@@ -497,6 +799,7 @@ var dinoGameMeta = defineFrameMeta({
|
|
|
497
799
|
});
|
|
498
800
|
var imageMeta = defineFrameMeta({
|
|
499
801
|
name: "image",
|
|
802
|
+
category: "layout",
|
|
500
803
|
iconUrl: widgetIcon("image"),
|
|
501
804
|
layout: { w: 3, h: 3, minW: 1, minH: 1 },
|
|
502
805
|
description: "Displays an image from a URL \u2014 logos, memes, chart screenshots, banners. Needs no data provider.",
|
|
@@ -511,6 +814,7 @@ var imageMeta = defineFrameMeta({
|
|
|
511
814
|
});
|
|
512
815
|
var headingMeta = defineFrameMeta({
|
|
513
816
|
name: "heading",
|
|
817
|
+
category: "layout",
|
|
514
818
|
iconUrl: widgetIcon("heading"),
|
|
515
819
|
layout: { w: 12, h: 1, minW: 2, minH: 1, maxH: 1 },
|
|
516
820
|
description: "Section divider that titles a region of the dashboard ('Markets', 'On-chain', 'Desk'). Renders as a label with a hairline rule \u2014 no card. Use to group frames into zones: place full-width (w: 12) and 1 row tall (h: 1) above each group. Needs no data provider.",
|
|
@@ -523,6 +827,7 @@ var headingMeta = defineFrameMeta({
|
|
|
523
827
|
});
|
|
524
828
|
var dailyAnalysisMeta = defineFrameMeta({
|
|
525
829
|
name: "daily-analysis",
|
|
830
|
+
category: "tools",
|
|
526
831
|
iconUrl: widgetIcon("daily-analysis"),
|
|
527
832
|
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
528
833
|
description: "Daily market brief written by the /zframes-brief loop \u2014 a dated analysis of the symbols on your dashboard, the calls it is making today, and how yesterday's calls scored (with a running hit-rate). Reads a local log file the loop appends to; needs no market data provider. Add one per dashboard.",
|
|
@@ -539,8 +844,51 @@ var dailyAnalysisMeta = defineFrameMeta({
|
|
|
539
844
|
)
|
|
540
845
|
})
|
|
541
846
|
});
|
|
847
|
+
var journalLogMeta = defineFrameMeta({
|
|
848
|
+
name: "journal-log",
|
|
849
|
+
category: "journal",
|
|
850
|
+
iconUrl: widgetIcon("journal-log"),
|
|
851
|
+
layout: { w: 4, h: 5, minW: 3, minH: 4 },
|
|
852
|
+
source: SOURCES.hyperliquid,
|
|
853
|
+
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.",
|
|
854
|
+
capabilities: ["quote-stream", "day-stats"],
|
|
855
|
+
schema: z3.object({})
|
|
856
|
+
});
|
|
857
|
+
var journalOpenMeta = defineFrameMeta({
|
|
858
|
+
name: "journal-open",
|
|
859
|
+
category: "journal",
|
|
860
|
+
iconUrl: widgetIcon("journal-open"),
|
|
861
|
+
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
862
|
+
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.",
|
|
863
|
+
source: SOURCES.hyperliquid,
|
|
864
|
+
capabilities: ["quote-stream"],
|
|
865
|
+
schema: z3.object({
|
|
866
|
+
max: z3.number().int().min(1).max(20).default(8).describe("How many open calls to show (newest first).")
|
|
867
|
+
})
|
|
868
|
+
});
|
|
869
|
+
var journalResultsMeta = defineFrameMeta({
|
|
870
|
+
name: "journal-results",
|
|
871
|
+
category: "journal",
|
|
872
|
+
iconUrl: widgetIcon("journal-results"),
|
|
873
|
+
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
874
|
+
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.",
|
|
875
|
+
capabilities: [],
|
|
876
|
+
schema: z3.object({
|
|
877
|
+
max: z3.number().int().min(1).max(20).default(8).describe("How many resolved calls to show (newest first).")
|
|
878
|
+
})
|
|
879
|
+
});
|
|
880
|
+
var journalScoreMeta = defineFrameMeta({
|
|
881
|
+
name: "journal-score",
|
|
882
|
+
category: "journal",
|
|
883
|
+
iconUrl: widgetIcon("journal-score"),
|
|
884
|
+
layout: { w: 4, h: 2, minW: 3, minH: 2 },
|
|
885
|
+
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.",
|
|
886
|
+
capabilities: [],
|
|
887
|
+
schema: z3.object({})
|
|
888
|
+
});
|
|
542
889
|
var priceCompareMeta = defineFrameMeta({
|
|
543
890
|
name: "price-compare",
|
|
891
|
+
category: "markets",
|
|
544
892
|
iconUrl: widgetIcon("price-compare"),
|
|
545
893
|
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
546
894
|
description: "Multi-series line chart overlaying the price history of several symbols over a lookback window \u2014 see how TSLA, NVDA and BTC moved against each other. Normalized by default to % change from the window start so symbols at very different price levels (BTC vs a $20 stock) stay comparable on one axis. Candles from Hyperliquid.",
|
|
@@ -556,26 +904,53 @@ var priceCompareMeta = defineFrameMeta({
|
|
|
556
904
|
)
|
|
557
905
|
})
|
|
558
906
|
});
|
|
559
|
-
var
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
907
|
+
var portfolioConfigShape = {
|
|
908
|
+
source: z3.enum(["binance", "wallet"]).default("binance").describe(
|
|
909
|
+
'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).'
|
|
910
|
+
),
|
|
911
|
+
address: z3.string().default("").describe(
|
|
912
|
+
'For source "wallet": the public Ethereum address (0x\u2026) or ENS name to track. Public on-chain data, no keys. Ignored for "binance".'
|
|
913
|
+
)
|
|
914
|
+
};
|
|
915
|
+
var portfolioValueMeta = defineFrameMeta({
|
|
916
|
+
name: "portfolio-value",
|
|
917
|
+
category: "portfolio",
|
|
918
|
+
iconUrl: widgetIcon("portfolio-value"),
|
|
919
|
+
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
920
|
+
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.",
|
|
921
|
+
capabilities: ["portfolio", "quote-stream"],
|
|
922
|
+
account: true,
|
|
566
923
|
schema: z3.object({
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
"Units held (e.g. 0.5 BTC, 10 shares). Weights the slice by USD value = amount \xD7 live price."
|
|
572
|
-
)
|
|
573
|
-
})
|
|
574
|
-
).min(2).max(8).describe("The holdings to chart. 2 to 8 positions.")
|
|
924
|
+
...portfolioConfigShape,
|
|
925
|
+
windowSec: z3.number().int().positive().default(300).describe(
|
|
926
|
+
"Seconds of live history the equity line shows; it accumulates from when the dashboard opens."
|
|
927
|
+
)
|
|
575
928
|
})
|
|
576
929
|
});
|
|
930
|
+
var portfolioAllocationMeta = defineFrameMeta({
|
|
931
|
+
name: "portfolio-allocation",
|
|
932
|
+
category: "portfolio",
|
|
933
|
+
iconUrl: widgetIcon("portfolio-allocation"),
|
|
934
|
+
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
935
|
+
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.",
|
|
936
|
+
capabilities: ["portfolio", "quote-stream"],
|
|
937
|
+
account: true,
|
|
938
|
+
schema: z3.object({ ...portfolioConfigShape })
|
|
939
|
+
});
|
|
940
|
+
var portfolioHoldingsMeta = defineFrameMeta({
|
|
941
|
+
name: "portfolio-holdings",
|
|
942
|
+
category: "portfolio",
|
|
943
|
+
iconUrl: widgetIcon("portfolio-holdings"),
|
|
944
|
+
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
945
|
+
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.",
|
|
946
|
+
capabilities: ["portfolio", "quote-stream"],
|
|
947
|
+
account: true,
|
|
948
|
+
schema: z3.object({ ...portfolioConfigShape })
|
|
949
|
+
});
|
|
577
950
|
var newsFeedMeta = defineFrameMeta({
|
|
578
951
|
name: "news-feed",
|
|
952
|
+
category: "sentiment",
|
|
953
|
+
iconUrl: widgetIcon("news-feed"),
|
|
579
954
|
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
580
955
|
description: 'Scrolling feed of the latest news headlines from a chosen outlet \u2014 each row is a clickable headline with its publish time, newest first. Free, keyless RSS sources: crypto press (CoinDesk, Cointelegraph, Decrypt), broad markets/macro (CNBC, Nasdaq), or \u2014 source "stocks" \u2014 per-company headlines (via Google News) scoped to the specific tickers in `symbols`. IMPORTANT: news feeds are CORS-blocked, so this frame reads them through the zframes runtime\'s data proxy (it ships with `zframes serve` / `vite dev`); on a fully static host with no runtime it shows an empty state.',
|
|
581
956
|
capabilities: ["news"],
|
|
@@ -598,6 +973,8 @@ var newsFeedMeta = defineFrameMeta({
|
|
|
598
973
|
});
|
|
599
974
|
var dexVolumeTreemapMeta = defineFrameMeta({
|
|
600
975
|
name: "dex-volume-treemap",
|
|
976
|
+
category: "crypto",
|
|
977
|
+
iconUrl: widgetIcon("dex-volume-treemap"),
|
|
601
978
|
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
602
979
|
description: "Treemap of decentralized-exchange (DEX) protocols sized by trailing-24h trading volume, tiles colored green/red by 1-day change. Data from DeFiLlama. One-glance read on where on-chain trading flow is concentrated right now.",
|
|
603
980
|
capabilities: ["dex-volume"],
|
|
@@ -608,6 +985,8 @@ var dexVolumeTreemapMeta = defineFrameMeta({
|
|
|
608
985
|
});
|
|
609
986
|
var dexVolumeChartMeta = defineFrameMeta({
|
|
610
987
|
name: "dex-volume-chart",
|
|
988
|
+
category: "crypto",
|
|
989
|
+
iconUrl: widgetIcon("dex-volume-chart"),
|
|
611
990
|
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
612
991
|
description: "Multi-series line chart of daily DEX trading volume for several protocols over a lookback window \u2014 compare how Uniswap, PancakeSwap, Aerodrome etc. trend against each other. Data from DeFiLlama (daily granularity).",
|
|
613
992
|
capabilities: ["dex-volume"],
|
|
@@ -621,6 +1000,8 @@ var dexVolumeChartMeta = defineFrameMeta({
|
|
|
621
1000
|
});
|
|
622
1001
|
var protocolTvlTreemapMeta = defineFrameMeta({
|
|
623
1002
|
name: "protocol-tvl-treemap",
|
|
1003
|
+
category: "crypto",
|
|
1004
|
+
iconUrl: widgetIcon("protocol-tvl-treemap"),
|
|
624
1005
|
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
625
1006
|
description: "Treemap of DeFi protocols sized by current total value locked (TVL), tiles colored green/red by 1-day change. Data from DeFiLlama. Unlike tvl-treemap (which groups by blockchain), this ranks individual protocols (Lido, Aave, EigenLayer\u2026).",
|
|
626
1007
|
capabilities: ["protocol-tvl"],
|
|
@@ -631,6 +1012,8 @@ var protocolTvlTreemapMeta = defineFrameMeta({
|
|
|
631
1012
|
});
|
|
632
1013
|
var protocolTvlChartMeta = defineFrameMeta({
|
|
633
1014
|
name: "protocol-tvl-chart",
|
|
1015
|
+
category: "crypto",
|
|
1016
|
+
iconUrl: widgetIcon("protocol-tvl-chart"),
|
|
634
1017
|
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
635
1018
|
description: "Multi-series line chart of total value locked (TVL) for several DeFi protocols over a lookback window. Data from DeFiLlama (daily granularity).",
|
|
636
1019
|
capabilities: ["protocol-tvl"],
|
|
@@ -644,6 +1027,8 @@ var protocolTvlChartMeta = defineFrameMeta({
|
|
|
644
1027
|
});
|
|
645
1028
|
var protocolFeesTreemapMeta = defineFrameMeta({
|
|
646
1029
|
name: "protocol-fees-treemap",
|
|
1030
|
+
category: "crypto",
|
|
1031
|
+
iconUrl: widgetIcon("protocol-fees-treemap"),
|
|
647
1032
|
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
648
1033
|
description: "Treemap of protocols sized by the fees they generated in the last 24h, tiles colored green/red by 1-day change. Data from DeFiLlama. Shows where on-chain users are actually paying for blockspace and services right now.",
|
|
649
1034
|
capabilities: ["protocol-fees"],
|
|
@@ -654,18 +1039,22 @@ var protocolFeesTreemapMeta = defineFrameMeta({
|
|
|
654
1039
|
});
|
|
655
1040
|
var marketCapTreemapMeta = defineFrameMeta({
|
|
656
1041
|
name: "market-cap-treemap",
|
|
1042
|
+
category: "crypto",
|
|
1043
|
+
iconUrl: widgetIcon("market-cap-treemap"),
|
|
657
1044
|
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
658
1045
|
description: "Treemap of the largest cryptocurrencies sized by market capitalisation, tiles colored green/red by 24h price change. Data from CoinGecko (free tier). A heat-map of the whole crypto market at a glance.",
|
|
659
1046
|
capabilities: ["coin-markets"],
|
|
660
1047
|
source: SOURCES.coingecko,
|
|
661
1048
|
schema: z3.object({
|
|
662
|
-
topN: z3.number().int().min(5).max(50).default(
|
|
1049
|
+
topN: z3.number().int().min(5).max(50).default(12).describe(
|
|
663
1050
|
"How many of the largest coins by market cap to show (up to 50)."
|
|
664
1051
|
)
|
|
665
1052
|
})
|
|
666
1053
|
});
|
|
667
1054
|
var openInterestMeta = defineFrameMeta({
|
|
668
1055
|
name: "open-interest",
|
|
1056
|
+
category: "derivatives",
|
|
1057
|
+
iconUrl: widgetIcon("open-interest"),
|
|
669
1058
|
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
670
1059
|
description: 'Live open interest across a watchlist of Hyperliquid perps \u2014 each symbol is a horizontal bar sized by USD notional, largest first, refreshed on a ~30s poll. Single-venue (Hyperliquid only), so read it as a relative gauge across your symbols, not a market-wide total. Stocks (HIP-3, e.g. "xyz:TSLA") and crypto both work.',
|
|
671
1060
|
capabilities: ["open-interest"],
|
|
@@ -678,6 +1067,8 @@ var openInterestMeta = defineFrameMeta({
|
|
|
678
1067
|
});
|
|
679
1068
|
var snakeMeta = defineFrameMeta({
|
|
680
1069
|
name: "snake",
|
|
1070
|
+
category: "games",
|
|
1071
|
+
iconUrl: widgetIcon("snake"),
|
|
681
1072
|
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
682
1073
|
description: "Classic snake game on canvas \u2014 steer with the arrow keys (or swipe), eat dots to grow, avoid the walls and your own tail. High score persists locally. For when the market is flat. Needs no data provider.",
|
|
683
1074
|
capabilities: [],
|
|
@@ -685,6 +1076,8 @@ var snakeMeta = defineFrameMeta({
|
|
|
685
1076
|
});
|
|
686
1077
|
var flappyBirdMeta = defineFrameMeta({
|
|
687
1078
|
name: "flappy-bird",
|
|
1079
|
+
category: "games",
|
|
1080
|
+
iconUrl: widgetIcon("flappy-bird"),
|
|
688
1081
|
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
689
1082
|
description: "Flappy-bird style game on canvas \u2014 tap or press SPACE to flap through the gaps between pipes. High score persists locally. Needs no data provider.",
|
|
690
1083
|
capabilities: [],
|
|
@@ -692,6 +1085,8 @@ var flappyBirdMeta = defineFrameMeta({
|
|
|
692
1085
|
});
|
|
693
1086
|
var videoMeta = defineFrameMeta({
|
|
694
1087
|
name: "video",
|
|
1088
|
+
category: "layout",
|
|
1089
|
+
iconUrl: widgetIcon("video"),
|
|
695
1090
|
layout: { w: 4, h: 3, minW: 2, minH: 2 },
|
|
696
1091
|
description: "Embeds a video from a YouTube or Vimeo link (or any direct embed URL) as an iframe \u2014 a livestream, a market-news clip, a focus playlist. Needs no data provider.",
|
|
697
1092
|
capabilities: [],
|
|
@@ -704,6 +1099,8 @@ var videoMeta = defineFrameMeta({
|
|
|
704
1099
|
});
|
|
705
1100
|
var drawdyMeta = defineFrameMeta({
|
|
706
1101
|
name: "drawdy",
|
|
1102
|
+
category: "layout",
|
|
1103
|
+
iconUrl: widgetIcon("drawdy"),
|
|
707
1104
|
layout: { w: 8, h: 6, minW: 2, minH: 2 },
|
|
708
1105
|
description: "Embeds drawdy.io as an interactive whiteboard canvas. No configuration needed.",
|
|
709
1106
|
capabilities: [],
|
|
@@ -711,6 +1108,8 @@ var drawdyMeta = defineFrameMeta({
|
|
|
711
1108
|
});
|
|
712
1109
|
var countdownMeta = defineFrameMeta({
|
|
713
1110
|
name: "countdown",
|
|
1111
|
+
category: "tools",
|
|
1112
|
+
iconUrl: widgetIcon("countdown"),
|
|
714
1113
|
layout: { w: 3, h: 2, minW: 2, minH: 1 },
|
|
715
1114
|
description: "Live countdown to a target date and time \u2014 FOMC decisions, CPI prints, options expiry, earnings, a token unlock, the next market open. Counts down in days / hours / minutes / seconds, ticking every second, and flips to a 'reached' state once the moment passes. Needs no data provider.",
|
|
716
1115
|
capabilities: [],
|
|
@@ -726,6 +1125,8 @@ var countdownMeta = defineFrameMeta({
|
|
|
726
1125
|
});
|
|
727
1126
|
var linkGridMeta = defineFrameMeta({
|
|
728
1127
|
name: "link-grid",
|
|
1128
|
+
category: "tools",
|
|
1129
|
+
iconUrl: widgetIcon("link-grid"),
|
|
729
1130
|
layout: { w: 3, h: 2, minW: 2, minH: 1 },
|
|
730
1131
|
description: "A grid of quick-launch tiles linking to your favourite sites \u2014 TradingView, exchanges, news, docs, your own dashboards. Each tile opens in a new tab and shows the destination site's favicon by default (fetched keyless from a public favicon service), with an optional per-link icon override and a first-letter fallback. Needs no data provider.",
|
|
731
1132
|
capabilities: [],
|
|
@@ -755,6 +1156,8 @@ var linkGridMeta = defineFrameMeta({
|
|
|
755
1156
|
});
|
|
756
1157
|
var calculatorMeta = defineFrameMeta({
|
|
757
1158
|
name: "calculator",
|
|
1159
|
+
category: "tools",
|
|
1160
|
+
iconUrl: widgetIcon("calculator"),
|
|
758
1161
|
layout: { w: 3, h: 3, minW: 2, minH: 2 },
|
|
759
1162
|
description: "Position-size & risk calculator. Enter account size, risk-per-trade %, entry and stop price; it computes the dollars at risk, the per-unit risk, the position size (units) that respects that risk budget, the resulting position value, and whether the setup is long or short. All math runs client-side \u2014 no data provider. Inputs are editable live; the configured values are the starting point.",
|
|
760
1163
|
capabilities: [],
|
|
@@ -770,6 +1173,8 @@ var calculatorMeta = defineFrameMeta({
|
|
|
770
1173
|
});
|
|
771
1174
|
var quoteMeta = defineFrameMeta({
|
|
772
1175
|
name: "quote",
|
|
1176
|
+
category: "layout",
|
|
1177
|
+
iconUrl: widgetIcon("quote"),
|
|
773
1178
|
layout: { w: 4, h: 2, minW: 2, minH: 1 },
|
|
774
1179
|
description: 'Displays a market or trading quote, centered \u2014 set one or rotate through several. A calm bit of wall-art for the dashboard: trading maxims, reminders of your own rules, mantras. Write any attribution into the text itself (e.g. "\u2026 \u2014 Buffett"). Needs no data provider.',
|
|
775
1180
|
capabilities: [],
|
|
@@ -788,6 +1193,8 @@ var quoteMeta = defineFrameMeta({
|
|
|
788
1193
|
});
|
|
789
1194
|
var dividerMeta = defineFrameMeta({
|
|
790
1195
|
name: "divider",
|
|
1196
|
+
category: "layout",
|
|
1197
|
+
iconUrl: widgetIcon("divider"),
|
|
791
1198
|
layout: { w: 12, h: 1, minW: 1, minH: 1 },
|
|
792
1199
|
description: "A plain rule that separates regions of the dashboard, with an optional centered label. Renders chrome-less (no card) \u2014 lighter than a heading. Use a horizontal divider full-width between stacked zones, or set orientation to vertical for a 1-column-wide column separator. Needs no data provider.",
|
|
793
1200
|
capabilities: [],
|
|
@@ -802,8 +1209,228 @@ var dividerMeta = defineFrameMeta({
|
|
|
802
1209
|
style: z3.enum(["solid", "dashed", "dotted"]).default("solid").describe("Line style.")
|
|
803
1210
|
})
|
|
804
1211
|
});
|
|
1212
|
+
var diceMeta = defineFrameMeta({
|
|
1213
|
+
name: "dice",
|
|
1214
|
+
category: "tools",
|
|
1215
|
+
iconUrl: widgetIcon("dice"),
|
|
1216
|
+
layout: { w: 2, h: 2, minW: 1, minH: 1 },
|
|
1217
|
+
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.",
|
|
1218
|
+
capabilities: [],
|
|
1219
|
+
schema: z3.object({
|
|
1220
|
+
mode: z3.enum(["coin", "dice", "list"]).default("coin").describe(
|
|
1221
|
+
"coin = heads/tails, dice = 1\u20136, list = random pick from options."
|
|
1222
|
+
),
|
|
1223
|
+
options: z3.array(z3.string()).default(["Yes", "No"]).describe("Choices used in list mode."),
|
|
1224
|
+
label: z3.string().default("").describe("Optional caption, e.g. the question being decided.")
|
|
1225
|
+
})
|
|
1226
|
+
});
|
|
1227
|
+
var riskRewardMeta = defineFrameMeta({
|
|
1228
|
+
name: "risk-reward",
|
|
1229
|
+
category: "tools",
|
|
1230
|
+
iconUrl: widgetIcon("risk-reward"),
|
|
1231
|
+
layout: { w: 3, h: 3, minW: 2, minH: 2 },
|
|
1232
|
+
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.",
|
|
1233
|
+
capabilities: [],
|
|
1234
|
+
schema: z3.object({
|
|
1235
|
+
entry: z3.number().default(100).describe("Planned entry price."),
|
|
1236
|
+
stop: z3.number().default(95).describe("Stop-loss price."),
|
|
1237
|
+
target: z3.number().default(115).describe("Profit target price."),
|
|
1238
|
+
direction: z3.enum(["long", "short"]).default("long").describe(
|
|
1239
|
+
"Trade direction. Long expects stop < entry < target; short expects target < entry < stop \u2014 used for labels and to flag a mismatched setup."
|
|
1240
|
+
),
|
|
1241
|
+
label: z3.string().default("").describe("Optional caption.")
|
|
1242
|
+
})
|
|
1243
|
+
});
|
|
1244
|
+
var marqueeMeta = defineFrameMeta({
|
|
1245
|
+
name: "marquee",
|
|
1246
|
+
category: "layout",
|
|
1247
|
+
iconUrl: widgetIcon("marquee"),
|
|
1248
|
+
layout: { w: 6, h: 1, minW: 2, minH: 1 },
|
|
1249
|
+
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.",
|
|
1250
|
+
capabilities: [],
|
|
1251
|
+
chrome: "bare",
|
|
1252
|
+
schema: z3.object({
|
|
1253
|
+
text: z3.string().default("LFG").describe("The text that scrolls across the banner."),
|
|
1254
|
+
speed: z3.enum(["slow", "normal", "fast"]).default("normal").describe("Scroll speed."),
|
|
1255
|
+
accent: z3.boolean().default(true).describe("Tint the text with the dashboard accent color.")
|
|
1256
|
+
})
|
|
1257
|
+
});
|
|
1258
|
+
var stopwatchMeta = defineFrameMeta({
|
|
1259
|
+
name: "stopwatch",
|
|
1260
|
+
layout: { w: 3, h: 2, minW: 2, minH: 1 },
|
|
1261
|
+
category: "tools",
|
|
1262
|
+
iconUrl: widgetIcon("stopwatch"),
|
|
1263
|
+
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.",
|
|
1264
|
+
capabilities: [],
|
|
1265
|
+
schema: z3.object({
|
|
1266
|
+
label: z3.string().default("Session").describe("Caption above the timer."),
|
|
1267
|
+
startedAt: z3.number().default(0).describe(
|
|
1268
|
+
"Epoch ms when the timer was last started; 0 = paused. Persisted automatically by the frame."
|
|
1269
|
+
),
|
|
1270
|
+
accumulatedMs: z3.number().default(0).describe(
|
|
1271
|
+
"Milliseconds banked before the current run. Persisted automatically by the frame."
|
|
1272
|
+
)
|
|
1273
|
+
})
|
|
1274
|
+
});
|
|
1275
|
+
var sessionProgressMeta = defineFrameMeta({
|
|
1276
|
+
name: "session-progress",
|
|
1277
|
+
category: "tools",
|
|
1278
|
+
iconUrl: widgetIcon("session-progress"),
|
|
1279
|
+
layout: { w: 3, h: 2, minW: 2, minH: 1 },
|
|
1280
|
+
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.",
|
|
1281
|
+
capabilities: [],
|
|
1282
|
+
schema: z3.object({
|
|
1283
|
+
exchange: z3.string().default("NYSE").describe("Exchange code: NYSE, NASDAQ, LSE, TSX, B3, \u2026"),
|
|
1284
|
+
label: z3.string().default("").describe("Optional caption shown by the exchange code."),
|
|
1285
|
+
showCountdown: z3.boolean().default(true).describe("Show the time-to-open / time-to-close countdown.")
|
|
1286
|
+
})
|
|
1287
|
+
});
|
|
1288
|
+
var holidayCalendarMeta = defineFrameMeta({
|
|
1289
|
+
name: "holiday-calendar",
|
|
1290
|
+
category: "tools",
|
|
1291
|
+
iconUrl: widgetIcon("holiday-calendar"),
|
|
1292
|
+
layout: { w: 3, h: 4, minW: 2, minH: 2 },
|
|
1293
|
+
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.",
|
|
1294
|
+
capabilities: [],
|
|
1295
|
+
schema: z3.object({
|
|
1296
|
+
exchange: z3.string().default("NYSE").describe("Exchange code: NYSE, NASDAQ, LSE, TSX, B3, \u2026"),
|
|
1297
|
+
count: z3.number().int().min(1).max(20).default(5).describe("How many upcoming holidays to list."),
|
|
1298
|
+
label: z3.string().default("").describe("Optional caption shown above the list.")
|
|
1299
|
+
})
|
|
1300
|
+
});
|
|
1301
|
+
var dayMeterMeta = defineFrameMeta({
|
|
1302
|
+
name: "day-meter",
|
|
1303
|
+
category: "tools",
|
|
1304
|
+
iconUrl: widgetIcon("day-meter"),
|
|
1305
|
+
layout: { w: 4, h: 2, minW: 2, minH: 1 },
|
|
1306
|
+
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.",
|
|
1307
|
+
capabilities: [],
|
|
1308
|
+
schema: z3.object({
|
|
1309
|
+
exchange: z3.string().default("NYSE").describe("Exchange code: NYSE, NASDAQ, LSE, TSX, B3, \u2026"),
|
|
1310
|
+
weekdaysOnly: z3.boolean().default(true).describe(
|
|
1311
|
+
"Show only the exchange's trading days; off shows the full 7-day week with weekends greyed."
|
|
1312
|
+
),
|
|
1313
|
+
label: z3.string().default("").describe("Optional caption shown by the strip.")
|
|
1314
|
+
})
|
|
1315
|
+
});
|
|
1316
|
+
var returnsProjectorMeta = defineFrameMeta({
|
|
1317
|
+
name: "returns-projector",
|
|
1318
|
+
category: "tools",
|
|
1319
|
+
iconUrl: widgetIcon("returns-projector"),
|
|
1320
|
+
layout: { w: 3, h: 4, minW: 2, minH: 3 },
|
|
1321
|
+
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`.",
|
|
1322
|
+
capabilities: [],
|
|
1323
|
+
schema: z3.object({
|
|
1324
|
+
principal: z3.number().default(1e3).describe("Starting balance."),
|
|
1325
|
+
ratePct: z3.number().default(5).describe("Return per period, in percent (e.g. 5 = 5%)."),
|
|
1326
|
+
periods: z3.number().int().min(1).max(600).default(12).describe("Number of compounding periods to project."),
|
|
1327
|
+
contribution: z3.number().default(0).describe("Amount added at the end of each period."),
|
|
1328
|
+
label: z3.string().default("").describe("Optional caption.")
|
|
1329
|
+
})
|
|
1330
|
+
});
|
|
1331
|
+
var breakevenMeta = defineFrameMeta({
|
|
1332
|
+
name: "breakeven",
|
|
1333
|
+
category: "tools",
|
|
1334
|
+
iconUrl: widgetIcon("breakeven"),
|
|
1335
|
+
layout: { w: 3, h: 4, minW: 2, minH: 2 },
|
|
1336
|
+
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.",
|
|
1337
|
+
capabilities: [],
|
|
1338
|
+
schema: z3.object({
|
|
1339
|
+
fills: z3.array(
|
|
1340
|
+
z3.object({
|
|
1341
|
+
price: z3.number().describe("Fill price."),
|
|
1342
|
+
size: z3.number().describe("Fill size, in units.")
|
|
1343
|
+
})
|
|
1344
|
+
).default([{ price: 100, size: 1 }]).describe(
|
|
1345
|
+
"Your entry fills; their size-weighted average is the break-even."
|
|
1346
|
+
),
|
|
1347
|
+
currentPrice: z3.number().default(0).describe(
|
|
1348
|
+
"Optional current price; greater than 0 shows the unrealized P&L %."
|
|
1349
|
+
),
|
|
1350
|
+
label: z3.string().default("").describe("Optional caption.")
|
|
1351
|
+
})
|
|
1352
|
+
});
|
|
1353
|
+
var checklistMeta = defineFrameMeta({
|
|
1354
|
+
name: "checklist",
|
|
1355
|
+
category: "tools",
|
|
1356
|
+
iconUrl: widgetIcon("checklist"),
|
|
1357
|
+
layout: { w: 3, h: 3, minW: 2, minH: 2 },
|
|
1358
|
+
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.",
|
|
1359
|
+
capabilities: [],
|
|
1360
|
+
schema: z3.object({
|
|
1361
|
+
title: z3.string().default("Pre-trade checklist").describe("Heading shown above the list."),
|
|
1362
|
+
items: z3.array(z3.string()).default([
|
|
1363
|
+
"Trend & bias aligned",
|
|
1364
|
+
"Stop level set",
|
|
1365
|
+
"Risk sized correctly"
|
|
1366
|
+
]).describe("The checklist items, top to bottom."),
|
|
1367
|
+
checked: z3.array(z3.boolean()).default([]).describe(
|
|
1368
|
+
"Per-item checked state (by index); persisted automatically by the frame."
|
|
1369
|
+
)
|
|
1370
|
+
})
|
|
1371
|
+
});
|
|
1372
|
+
var pomodoroMeta = defineFrameMeta({
|
|
1373
|
+
name: "pomodoro",
|
|
1374
|
+
category: "tools",
|
|
1375
|
+
iconUrl: widgetIcon("pomodoro"),
|
|
1376
|
+
layout: { w: 3, h: 3, minW: 2, minH: 2 },
|
|
1377
|
+
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).",
|
|
1378
|
+
capabilities: [],
|
|
1379
|
+
schema: z3.object({
|
|
1380
|
+
workMin: z3.number().min(1).max(180).default(25).describe("Work interval length, in minutes."),
|
|
1381
|
+
breakMin: z3.number().min(1).max(120).default(5).describe("Break interval length, in minutes."),
|
|
1382
|
+
cycles: z3.number().int().min(1).max(20).default(4).describe("Work/break cycles before the counter loops."),
|
|
1383
|
+
label: z3.string().default("").describe("Optional caption.")
|
|
1384
|
+
})
|
|
1385
|
+
});
|
|
1386
|
+
var rulesCardMeta = defineFrameMeta({
|
|
1387
|
+
name: "rules-card",
|
|
1388
|
+
category: "layout",
|
|
1389
|
+
iconUrl: widgetIcon("rules-card"),
|
|
1390
|
+
layout: { w: 3, h: 3, minW: 2, minH: 2 },
|
|
1391
|
+
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.",
|
|
1392
|
+
capabilities: [],
|
|
1393
|
+
schema: z3.object({
|
|
1394
|
+
title: z3.string().default("My rules").describe("Heading shown above the list."),
|
|
1395
|
+
rules: z3.array(z3.string()).default([
|
|
1396
|
+
"Cut losers fast, let winners run",
|
|
1397
|
+
"No trade without a stop",
|
|
1398
|
+
"One setup at a time"
|
|
1399
|
+
]).describe("The rules, in order; rendered as a numbered list.")
|
|
1400
|
+
})
|
|
1401
|
+
});
|
|
1402
|
+
var breathingMeta = defineFrameMeta({
|
|
1403
|
+
name: "breathing",
|
|
1404
|
+
category: "layout",
|
|
1405
|
+
iconUrl: widgetIcon("breathing"),
|
|
1406
|
+
chrome: "bare",
|
|
1407
|
+
layout: { w: 2, h: 2, minW: 1, minH: 1 },
|
|
1408
|
+
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.",
|
|
1409
|
+
capabilities: [],
|
|
1410
|
+
schema: z3.object({
|
|
1411
|
+
inhale: z3.number().min(1).max(60).default(4).describe("Inhale seconds."),
|
|
1412
|
+
hold: z3.number().min(0).max(60).default(4).describe("Hold-after-inhale seconds."),
|
|
1413
|
+
exhale: z3.number().min(1).max(60).default(4).describe("Exhale seconds."),
|
|
1414
|
+
holdAfter: z3.number().min(0).max(60).default(4).describe("Hold-after-exhale seconds.")
|
|
1415
|
+
})
|
|
1416
|
+
});
|
|
1417
|
+
var spotifyEmbedMeta = defineFrameMeta({
|
|
1418
|
+
name: "spotify-embed",
|
|
1419
|
+
category: "layout",
|
|
1420
|
+
iconUrl: widgetIcon("spotify-embed"),
|
|
1421
|
+
layout: { w: 3, h: 4, minW: 2, minH: 2 },
|
|
1422
|
+
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.",
|
|
1423
|
+
capabilities: [],
|
|
1424
|
+
schema: z3.object({
|
|
1425
|
+
url: z3.string().default("https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M").describe(
|
|
1426
|
+
"A Spotify share URL (open.spotify.com/track|album|playlist|artist/\u2026)."
|
|
1427
|
+
),
|
|
1428
|
+
compact: z3.boolean().default(false).describe("Use Spotify's compact (single-row) player height.")
|
|
1429
|
+
})
|
|
1430
|
+
});
|
|
805
1431
|
var btcFeesMeta = defineFrameMeta({
|
|
806
1432
|
name: "btc-fees",
|
|
1433
|
+
category: "bitcoin",
|
|
807
1434
|
iconUrl: widgetIcon("btc-fees"),
|
|
808
1435
|
layout: { w: 3, h: 2, minW: 2, minH: 2 },
|
|
809
1436
|
description: "Recommended Bitcoin on-chain fee rates (sat/vB) from mempool.space \u2014 the next-block ('fastest'), ~30-minute, ~1-hour, economy, and minimum tiers, as a compact gauge. Live mempool data, keyless; updates every ~30s.",
|
|
@@ -817,6 +1444,7 @@ var btcFeesMeta = defineFrameMeta({
|
|
|
817
1444
|
});
|
|
818
1445
|
var btcMempoolMeta = defineFrameMeta({
|
|
819
1446
|
name: "btc-mempool",
|
|
1447
|
+
category: "bitcoin",
|
|
820
1448
|
iconUrl: widgetIcon("btc-mempool"),
|
|
821
1449
|
layout: { w: 5, h: 3, minW: 3, minH: 2 },
|
|
822
1450
|
description: "Bitcoin mempool congestion at a glance \u2014 unconfirmed transaction count, total pending vsize, and a row of projected ('template') blocks the network will likely mine next, each labelled with its median fee rate (sat/vB) and tx count. Live mempool data from mempool.space, keyless.",
|
|
@@ -830,6 +1458,7 @@ var btcMempoolMeta = defineFrameMeta({
|
|
|
830
1458
|
});
|
|
831
1459
|
var btcBlocksMeta = defineFrameMeta({
|
|
832
1460
|
name: "btc-blocks",
|
|
1461
|
+
category: "bitcoin",
|
|
833
1462
|
iconUrl: widgetIcon("btc-blocks"),
|
|
834
1463
|
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
835
1464
|
description: "Feed of the most recently mined Bitcoin blocks \u2014 each row shows the height, how long ago it was mined, transaction count, the mining pool that found it, total fees (BTC), and size. Live data from mempool.space, keyless; newest first.",
|
|
@@ -841,6 +1470,7 @@ var btcBlocksMeta = defineFrameMeta({
|
|
|
841
1470
|
});
|
|
842
1471
|
var btcHashrateMeta = defineFrameMeta({
|
|
843
1472
|
name: "btc-hashrate",
|
|
1473
|
+
category: "bitcoin",
|
|
844
1474
|
iconUrl: widgetIcon("btc-hashrate"),
|
|
845
1475
|
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
846
1476
|
description: "Bitcoin network hashrate over time as a line chart, with the current hashrate (EH/s) and difficulty as headline figures. Shows the long-run security trend of the network. Data from mempool.space (daily granularity), keyless.",
|
|
@@ -852,6 +1482,7 @@ var btcHashrateMeta = defineFrameMeta({
|
|
|
852
1482
|
});
|
|
853
1483
|
var btcDifficultyMeta = defineFrameMeta({
|
|
854
1484
|
name: "btc-difficulty",
|
|
1485
|
+
category: "bitcoin",
|
|
855
1486
|
iconUrl: widgetIcon("btc-difficulty"),
|
|
856
1487
|
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
857
1488
|
description: "Countdown to the next Bitcoin difficulty adjustment \u2014 a progress bar through the current 2016-block epoch, the estimated change (+ = mining gets harder), blocks remaining, and the estimated retarget date. Also shows the previous adjustment. Data from mempool.space, keyless.",
|
|
@@ -865,6 +1496,7 @@ var btcDifficultyMeta = defineFrameMeta({
|
|
|
865
1496
|
});
|
|
866
1497
|
var miningPoolsMeta = defineFrameMeta({
|
|
867
1498
|
name: "mining-pools",
|
|
1499
|
+
category: "bitcoin",
|
|
868
1500
|
iconUrl: widgetIcon("mining-pools"),
|
|
869
1501
|
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
870
1502
|
description: "Treemap of Bitcoin mining-pool dominance over a window \u2014 each tile is a pool sized by the share of blocks it mined, so you can see how concentrated hashpower is right now (Foundry, AntPool, ViaBTC\u2026). Data from mempool.space, keyless.",
|
|
@@ -879,6 +1511,7 @@ var miningPoolsMeta = defineFrameMeta({
|
|
|
879
1511
|
});
|
|
880
1512
|
var lightningStatsMeta = defineFrameMeta({
|
|
881
1513
|
name: "lightning-stats",
|
|
1514
|
+
category: "bitcoin",
|
|
882
1515
|
iconUrl: widgetIcon("lightning-stats"),
|
|
883
1516
|
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
884
1517
|
description: "Bitcoin Lightning Network snapshot \u2014 public node count, channel count, and total network capacity (BTC), with a day-over-day delta and the Tor/clearnet node split. Data from mempool.space, keyless; updates roughly daily.",
|
|
@@ -892,6 +1525,7 @@ var lightningStatsMeta = defineFrameMeta({
|
|
|
892
1525
|
});
|
|
893
1526
|
var optionsPutCallMeta = defineFrameMeta({
|
|
894
1527
|
name: "options-put-call",
|
|
1528
|
+
category: "derivatives",
|
|
895
1529
|
iconUrl: widgetIcon("options-put-call"),
|
|
896
1530
|
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
897
1531
|
description: "Deribit options put/call ratio for BTC or ETH \u2014 the headline ratio (by open interest or 24h volume), a call-vs-put open-interest split bar, and the open-interest-weighted average implied volatility. A ratio above 1 means puts outweigh calls (defensive positioning). Keyless Deribit market data.",
|
|
@@ -908,6 +1542,7 @@ var optionsPutCallMeta = defineFrameMeta({
|
|
|
908
1542
|
});
|
|
909
1543
|
var optionsIvMeta = defineFrameMeta({
|
|
910
1544
|
name: "options-iv",
|
|
1545
|
+
category: "derivatives",
|
|
911
1546
|
iconUrl: widgetIcon("options-iv"),
|
|
912
1547
|
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
913
1548
|
description: "Deribit DVOL implied-volatility index for BTC or ETH over time \u2014 the crypto equivalent of the VIX, as a line chart with the current reading and its change over the window. Rising DVOL = the market is pricing bigger expected swings. Keyless Deribit market data.",
|
|
@@ -920,6 +1555,7 @@ var optionsIvMeta = defineFrameMeta({
|
|
|
920
1555
|
});
|
|
921
1556
|
var optionsOiStrikeMeta = defineFrameMeta({
|
|
922
1557
|
name: "options-oi-strike",
|
|
1558
|
+
category: "derivatives",
|
|
923
1559
|
iconUrl: widgetIcon("options-oi-strike"),
|
|
924
1560
|
layout: { w: 6, h: 4, minW: 4, minH: 3 },
|
|
925
1561
|
description: "Open interest by strike for the nearest Deribit options expiry (BTC or ETH) \u2014 a grouped histogram of call vs put open interest across strikes, with the current spot marked. Surfaces the strike 'walls' where positioning is concentrated. Keyless Deribit market data.",
|
|
@@ -934,6 +1570,7 @@ var optionsOiStrikeMeta = defineFrameMeta({
|
|
|
934
1570
|
});
|
|
935
1571
|
var coinMoversMeta = defineFrameMeta({
|
|
936
1572
|
name: "coin-movers",
|
|
1573
|
+
category: "markets",
|
|
937
1574
|
iconUrl: widgetIcon("coin-movers"),
|
|
938
1575
|
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
939
1576
|
description: "Broad-market crypto gainers and losers across the top ~300 coins by market cap, over a selectable window (1h / 24h / 7d / 30d) \u2014 the biggest movers side by side with price and % change. Unlike a top-coins heatmap, this surfaces mid- and small-caps that ripped or dumped, not just the megacaps. Keyless data from Coinpaprika. Crypto only.",
|
|
@@ -951,7 +1588,9 @@ var coinMoversMeta = defineFrameMeta({
|
|
|
951
1588
|
});
|
|
952
1589
|
var frameMetas = [
|
|
953
1590
|
newsFeedMeta,
|
|
954
|
-
|
|
1591
|
+
portfolioValueMeta,
|
|
1592
|
+
portfolioAllocationMeta,
|
|
1593
|
+
portfolioHoldingsMeta,
|
|
955
1594
|
bitcoinDominanceMeta,
|
|
956
1595
|
clockMeta,
|
|
957
1596
|
dailyAnalysisMeta,
|
|
@@ -975,6 +1614,7 @@ var frameMetas = [
|
|
|
975
1614
|
priceLivelineMeta,
|
|
976
1615
|
priceTickerMeta,
|
|
977
1616
|
ratesBoardMeta,
|
|
1617
|
+
fxBoardMeta,
|
|
978
1618
|
shortVolumeMeta,
|
|
979
1619
|
topMoversMeta,
|
|
980
1620
|
tvlTreemapMeta,
|
|
@@ -1007,15 +1647,87 @@ var frameMetas = [
|
|
|
1007
1647
|
optionsOiStrikeMeta,
|
|
1008
1648
|
coinMoversMeta
|
|
1009
1649
|
];
|
|
1650
|
+
var allFrameMetas = [
|
|
1651
|
+
...frameMetas,
|
|
1652
|
+
breakevenMeta,
|
|
1653
|
+
breathingMeta,
|
|
1654
|
+
checklistMeta,
|
|
1655
|
+
dayMeterMeta,
|
|
1656
|
+
diceMeta,
|
|
1657
|
+
holidayCalendarMeta,
|
|
1658
|
+
journalLogMeta,
|
|
1659
|
+
journalOpenMeta,
|
|
1660
|
+
journalResultsMeta,
|
|
1661
|
+
journalScoreMeta,
|
|
1662
|
+
marqueeMeta,
|
|
1663
|
+
pomodoroMeta,
|
|
1664
|
+
returnsProjectorMeta,
|
|
1665
|
+
riskRewardMeta,
|
|
1666
|
+
rulesCardMeta,
|
|
1667
|
+
sessionProgressMeta,
|
|
1668
|
+
spotifyEmbedMeta,
|
|
1669
|
+
stopwatchMeta
|
|
1670
|
+
];
|
|
1671
|
+
|
|
1672
|
+
// src/dashboards.ts
|
|
1673
|
+
import { existsSync } from "fs";
|
|
1674
|
+
function list() {
|
|
1675
|
+
const entries = listDashboards();
|
|
1676
|
+
if (entries.length === 0) {
|
|
1677
|
+
console.log(`no dashboards in your store yet (${dashboardsDir()})`);
|
|
1678
|
+
console.log(" create one: npx --yes zframes@latest init <name>");
|
|
1679
|
+
return 0;
|
|
1680
|
+
}
|
|
1681
|
+
console.log(`dashboards in ${dashboardsDir()}:`);
|
|
1682
|
+
for (const e of entries) {
|
|
1683
|
+
const mark = e.isDefault ? "*" : " ";
|
|
1684
|
+
const title = e.title ? ` \u2014 ${e.title}` : "";
|
|
1685
|
+
console.log(` ${mark} ${e.name}${title}`);
|
|
1686
|
+
}
|
|
1687
|
+
if (entries.some((e) => e.isDefault)) {
|
|
1688
|
+
console.log(
|
|
1689
|
+
"\n * = default (served when you run `zframes serve` with no name)"
|
|
1690
|
+
);
|
|
1691
|
+
} else {
|
|
1692
|
+
console.log("\n no default set \u2014 `zframes use <name>` to pick one.");
|
|
1693
|
+
}
|
|
1694
|
+
return 0;
|
|
1695
|
+
}
|
|
1696
|
+
function use(args) {
|
|
1697
|
+
const name = args.find((a) => !a.startsWith("-"));
|
|
1698
|
+
if (!name) {
|
|
1699
|
+
console.error("usage: zframes use <name>");
|
|
1700
|
+
return 1;
|
|
1701
|
+
}
|
|
1702
|
+
if (!isValidName(name)) {
|
|
1703
|
+
console.error(
|
|
1704
|
+
`\u2717 "${name}" is not a valid dashboard name (lowercase letters, digits, "-", "_")`
|
|
1705
|
+
);
|
|
1706
|
+
return 1;
|
|
1707
|
+
}
|
|
1708
|
+
if (!existsSync(dashboardPath(name))) {
|
|
1709
|
+
console.error(
|
|
1710
|
+
`\u2717 no dashboard named "${name}" in your store (${dashboardsDir()})`
|
|
1711
|
+
);
|
|
1712
|
+
console.error(
|
|
1713
|
+
` create it with \`zframes init ${name}\`, or run \`zframes list\` to see what's there.`
|
|
1714
|
+
);
|
|
1715
|
+
return 1;
|
|
1716
|
+
}
|
|
1717
|
+
setDefault(name);
|
|
1718
|
+
console.log(`\u2713 default dashboard is now "${name}"`);
|
|
1719
|
+
return 0;
|
|
1720
|
+
}
|
|
1010
1721
|
|
|
1011
1722
|
// src/init.ts
|
|
1012
|
-
import { existsSync, mkdirSync, statSync, writeFileSync } from "fs";
|
|
1013
|
-
import { dirname,
|
|
1723
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1724
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
1014
1725
|
function parseArgs(args) {
|
|
1015
1726
|
let target = "dashboard.json";
|
|
1016
1727
|
let title = "my dashboard";
|
|
1017
1728
|
let author = "";
|
|
1018
1729
|
let force = false;
|
|
1730
|
+
let makeDefault = false;
|
|
1019
1731
|
for (let i = 0; i < args.length; i++) {
|
|
1020
1732
|
const a = args[i];
|
|
1021
1733
|
if (a === "--title" || a === "-t") {
|
|
@@ -1028,6 +1740,8 @@ function parseArgs(args) {
|
|
|
1028
1740
|
author = a.slice("--author=".length);
|
|
1029
1741
|
} else if (a === "--force" || a === "-f") {
|
|
1030
1742
|
force = true;
|
|
1743
|
+
} else if (a === "--default") {
|
|
1744
|
+
makeDefault = true;
|
|
1031
1745
|
} else if (!a.startsWith("-")) {
|
|
1032
1746
|
target = a;
|
|
1033
1747
|
} else {
|
|
@@ -1035,12 +1749,14 @@ function parseArgs(args) {
|
|
|
1035
1749
|
}
|
|
1036
1750
|
}
|
|
1037
1751
|
if (!title.trim()) return { error: "--title cannot be empty" };
|
|
1038
|
-
return { target, title, author, force };
|
|
1752
|
+
return { target, title, author, force, makeDefault };
|
|
1039
1753
|
}
|
|
1040
1754
|
function resolveDest(target) {
|
|
1041
|
-
const
|
|
1042
|
-
if (
|
|
1043
|
-
|
|
1755
|
+
const c = classifyTarget(target, process.cwd());
|
|
1756
|
+
if ("error" in c) return c;
|
|
1757
|
+
if (c.kind === "store") return { dest: c.file, name: c.name };
|
|
1758
|
+
if (c.file.toLowerCase().endsWith(".json")) return { dest: c.file };
|
|
1759
|
+
return { dest: resolve2(c.file, "dashboard.json") };
|
|
1044
1760
|
}
|
|
1045
1761
|
function skeleton(title, author) {
|
|
1046
1762
|
return {
|
|
@@ -1053,7 +1769,19 @@ function skeleton(title, author) {
|
|
|
1053
1769
|
projectId: "YrTzGatwjK7EoFpCSfgZ",
|
|
1054
1770
|
opacity: 1
|
|
1055
1771
|
},
|
|
1056
|
-
theme: {
|
|
1772
|
+
theme: {
|
|
1773
|
+
accentHue: 242,
|
|
1774
|
+
accentSat: 90,
|
|
1775
|
+
baseHue: 233,
|
|
1776
|
+
baseSat: 20,
|
|
1777
|
+
upColor: "#3fd08f",
|
|
1778
|
+
downColor: "#ff6b81"
|
|
1779
|
+
},
|
|
1780
|
+
typography: {
|
|
1781
|
+
fontFamily: "sans",
|
|
1782
|
+
numericStyle: "proportional",
|
|
1783
|
+
scale: 1
|
|
1784
|
+
},
|
|
1057
1785
|
appearance: {
|
|
1058
1786
|
radius: 18,
|
|
1059
1787
|
borderStrength: 0.22,
|
|
@@ -1069,12 +1797,17 @@ function init(args) {
|
|
|
1069
1797
|
if ("error" in parsed) {
|
|
1070
1798
|
console.error(`\u2717 ${parsed.error}`);
|
|
1071
1799
|
console.error(
|
|
1072
|
-
"usage: zframes init [dir|file.json] [--title <t>] [--author <a>] [--force]"
|
|
1800
|
+
"usage: zframes init [name|dir|file.json] [--title <t>] [--author <a>] [--default] [--force]"
|
|
1073
1801
|
);
|
|
1074
1802
|
return 1;
|
|
1075
1803
|
}
|
|
1076
|
-
const
|
|
1077
|
-
if (
|
|
1804
|
+
const resolved = resolveDest(parsed.target);
|
|
1805
|
+
if ("error" in resolved) {
|
|
1806
|
+
console.error(`\u2717 ${resolved.error}`);
|
|
1807
|
+
return 1;
|
|
1808
|
+
}
|
|
1809
|
+
const { dest, name } = resolved;
|
|
1810
|
+
if (existsSync2(dest)) {
|
|
1078
1811
|
if (!parsed.force) {
|
|
1079
1812
|
console.error(`\u2717 ${dest} already exists`);
|
|
1080
1813
|
console.error(
|
|
@@ -1082,7 +1815,7 @@ function init(args) {
|
|
|
1082
1815
|
);
|
|
1083
1816
|
return 1;
|
|
1084
1817
|
}
|
|
1085
|
-
if (!
|
|
1818
|
+
if (!statSync2(dest).isFile()) {
|
|
1086
1819
|
console.error(`\u2717 ${dest} exists but is not a file`);
|
|
1087
1820
|
return 1;
|
|
1088
1821
|
}
|
|
@@ -1096,15 +1829,26 @@ function init(args) {
|
|
|
1096
1829
|
return 1;
|
|
1097
1830
|
}
|
|
1098
1831
|
try {
|
|
1099
|
-
|
|
1100
|
-
|
|
1832
|
+
if (name) ensureHome();
|
|
1833
|
+
mkdirSync2(dirname(dest), { recursive: true });
|
|
1834
|
+
writeFileSync2(dest, `${JSON.stringify(spec, null, 2)}
|
|
1101
1835
|
`);
|
|
1102
1836
|
} catch (error) {
|
|
1103
1837
|
console.error(`\u2717 could not write ${dest}: ${error.message}`);
|
|
1104
1838
|
return 1;
|
|
1105
1839
|
}
|
|
1106
|
-
|
|
1840
|
+
let madeDefault = false;
|
|
1841
|
+
if (name && (parsed.makeDefault || getDefault() === null)) {
|
|
1842
|
+
setDefault(name);
|
|
1843
|
+
madeDefault = true;
|
|
1844
|
+
}
|
|
1845
|
+
const hint = name ?? dest;
|
|
1107
1846
|
console.log(`\u2713 wrote a bare dashboard to ${dest}`);
|
|
1847
|
+
if (name) {
|
|
1848
|
+
console.log(
|
|
1849
|
+
` it's in your store (${dashboardsDir()})${madeDefault ? " and is now your default" : ""}.`
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1108
1852
|
console.log(" next: read the catalogue, add frames, then lint + serve:");
|
|
1109
1853
|
console.log(" npx --yes zframes@latest catalogue");
|
|
1110
1854
|
console.log(` npx --yes zframes@latest lint ${hint}`);
|
|
@@ -1148,6 +1892,12 @@ function lintSpec(spec) {
|
|
|
1148
1892
|
frameId: instance.id,
|
|
1149
1893
|
message: `overflows the grid: x(${instance.position.x}) + w(${instance.position.w}) > ${spec.grid.columns} columns`
|
|
1150
1894
|
});
|
|
1895
|
+
const horizontal = instance.layouts?.["flow-horizontal"];
|
|
1896
|
+
if (horizontal && horizontal.y + horizontal.h > spec.grid.rows)
|
|
1897
|
+
issues.push({
|
|
1898
|
+
frameId: instance.id,
|
|
1899
|
+
message: `horizontal layout overflows: y(${horizontal.y}) + h(${horizontal.h}) > ${spec.grid.rows} rows`
|
|
1900
|
+
});
|
|
1151
1901
|
}
|
|
1152
1902
|
for (let i = 0; i < spec.frames.length; i++) {
|
|
1153
1903
|
for (let j = i + 1; j < spec.frames.length; j++) {
|
|
@@ -1165,9 +1915,11 @@ function lintSpec(spec) {
|
|
|
1165
1915
|
}
|
|
1166
1916
|
|
|
1167
1917
|
// src/serve.ts
|
|
1168
|
-
import { existsSync as
|
|
1169
|
-
import {
|
|
1170
|
-
|
|
1918
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1919
|
+
import {
|
|
1920
|
+
createServer
|
|
1921
|
+
} from "http";
|
|
1922
|
+
import { join as join3, resolve as resolve3 } from "path";
|
|
1171
1923
|
import { fileURLToPath } from "url";
|
|
1172
1924
|
import sirv from "sirv";
|
|
1173
1925
|
|
|
@@ -1177,7 +1929,11 @@ import { readFile, writeFile } from "fs/promises";
|
|
|
1177
1929
|
// ../core/src/routes.ts
|
|
1178
1930
|
var DASHBOARD_READ_ROUTE = "/__zframes/dashboard.json";
|
|
1179
1931
|
var DASHBOARD_WRITE_ROUTE = "/__zframes/dashboard";
|
|
1932
|
+
var DASHBOARD_LIST_ROUTE = "/__zframes/dashboards";
|
|
1933
|
+
var DASHBOARD_SWITCH_ROUTE = "/__zframes/switch";
|
|
1180
1934
|
var DASHBOARD_PROXY_ROUTE = "/__zframes/proxy";
|
|
1935
|
+
var ACCOUNT_PORTFOLIO_ROUTE = "/__zframes/account/portfolio";
|
|
1936
|
+
var ACCOUNT_CREDENTIALS_ROUTE = "/__zframes/account/credentials";
|
|
1181
1937
|
var AGENTS_LIST_ROUTE = "/__zframes/agents";
|
|
1182
1938
|
var ASK_ROUTE = "/__zframes/ask";
|
|
1183
1939
|
|
|
@@ -1193,6 +1949,7 @@ var PROXY_ALLOW_HOSTS = /* @__PURE__ */ new Set([
|
|
|
1193
1949
|
"www.nyse.com",
|
|
1194
1950
|
"markets.newyorkfed.org",
|
|
1195
1951
|
"api.fiscaldata.treasury.gov",
|
|
1952
|
+
"home.treasury.gov",
|
|
1196
1953
|
"api.bls.gov",
|
|
1197
1954
|
"cdn.finra.org",
|
|
1198
1955
|
// News-outlet RSS feeds (CORS-blocked, so the news-feed frame reads them
|
|
@@ -1244,8 +2001,8 @@ function handleSpecWrite(req, res, absFile) {
|
|
|
1244
2001
|
req.on("end", async () => {
|
|
1245
2002
|
if (aborted) return;
|
|
1246
2003
|
try {
|
|
1247
|
-
const
|
|
1248
|
-
await writeFile(absFile, `${JSON.stringify(
|
|
2004
|
+
const json2 = JSON.parse(body);
|
|
2005
|
+
await writeFile(absFile, `${JSON.stringify(json2, null, 2)}
|
|
1249
2006
|
`, "utf8");
|
|
1250
2007
|
res.statusCode = 200;
|
|
1251
2008
|
res.setHeader("content-type", "application/json");
|
|
@@ -1318,14 +2075,59 @@ import { tmpdir } from "os";
|
|
|
1318
2075
|
import { dirname as dirname2, join as join2 } from "path";
|
|
1319
2076
|
var MAX_BODY_BYTES2 = 64e3;
|
|
1320
2077
|
var RUN_TIMEOUT_MS = 12e4;
|
|
2078
|
+
var MAX_CONTEXT_CHARS = 12e3;
|
|
2079
|
+
var MAX_HISTORY_TURNS = 6;
|
|
2080
|
+
var MAX_HISTORY_CHARS = 600;
|
|
2081
|
+
function tryParse(line) {
|
|
2082
|
+
const s = line.trim();
|
|
2083
|
+
if (!s) return null;
|
|
2084
|
+
try {
|
|
2085
|
+
return JSON.parse(s);
|
|
2086
|
+
} catch {
|
|
2087
|
+
return null;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
function claudeDelta(line) {
|
|
2091
|
+
const o = tryParse(line);
|
|
2092
|
+
if (o?.type !== "stream_event") return null;
|
|
2093
|
+
const delta = o.event?.type === "content_block_delta" ? o.event.delta : void 0;
|
|
2094
|
+
return delta?.type === "text_delta" && typeof delta.text === "string" ? delta.text : null;
|
|
2095
|
+
}
|
|
2096
|
+
function claudeResult(stdout) {
|
|
2097
|
+
let result = null;
|
|
2098
|
+
let deltas = "";
|
|
2099
|
+
for (const line of stdout.split("\n")) {
|
|
2100
|
+
const o = tryParse(line);
|
|
2101
|
+
if (!o) continue;
|
|
2102
|
+
if (o.type === "result" && typeof o.result === "string") {
|
|
2103
|
+
result = o.result;
|
|
2104
|
+
continue;
|
|
2105
|
+
}
|
|
2106
|
+
if (o.type === "stream_event" && o.event?.type === "content_block_delta" && o.event.delta?.type === "text_delta" && typeof o.event.delta.text === "string") {
|
|
2107
|
+
deltas += o.event.delta.text;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
return (result ?? deltas).trim();
|
|
2111
|
+
}
|
|
1321
2112
|
var RUNNERS = [
|
|
1322
2113
|
{
|
|
1323
2114
|
id: "claude",
|
|
1324
2115
|
label: "Claude",
|
|
1325
2116
|
bin: "claude",
|
|
1326
|
-
// -p/--print is non-interactive
|
|
1327
|
-
|
|
1328
|
-
|
|
2117
|
+
// -p/--print is non-interactive. stream-json emits NDJSON events as the
|
|
2118
|
+
// answer is generated; --verbose is required alongside it under -p, and
|
|
2119
|
+
// --include-partial-messages adds the token-level `content_block_delta`
|
|
2120
|
+
// events we relay live. The canonical answer is the closing `result` event.
|
|
2121
|
+
buildArgs: (prompt) => [
|
|
2122
|
+
"-p",
|
|
2123
|
+
prompt,
|
|
2124
|
+
"--output-format",
|
|
2125
|
+
"stream-json",
|
|
2126
|
+
"--verbose",
|
|
2127
|
+
"--include-partial-messages"
|
|
2128
|
+
],
|
|
2129
|
+
parseDelta: claudeDelta,
|
|
2130
|
+
readResult: async (stdout) => claudeResult(stdout)
|
|
1329
2131
|
},
|
|
1330
2132
|
{
|
|
1331
2133
|
id: "codex",
|
|
@@ -1382,7 +2184,7 @@ function detectAgents() {
|
|
|
1382
2184
|
}
|
|
1383
2185
|
return detected;
|
|
1384
2186
|
}
|
|
1385
|
-
async function buildPrompt(specFile, question) {
|
|
2187
|
+
async function buildPrompt(specFile, question, clientContext, catalogue, history) {
|
|
1386
2188
|
let title = "a live market dashboard";
|
|
1387
2189
|
const symbols = /* @__PURE__ */ new Set();
|
|
1388
2190
|
try {
|
|
@@ -1397,45 +2199,93 @@ async function buildPrompt(specFile, question) {
|
|
|
1397
2199
|
}
|
|
1398
2200
|
} catch {
|
|
1399
2201
|
}
|
|
1400
|
-
const
|
|
1401
|
-
|
|
2202
|
+
const trimmed = clientContext?.trim();
|
|
2203
|
+
const grounding = trimmed ? `Here is what the user is looking at on their dashboard "${title}" right now (live values captured from the screen):
|
|
2204
|
+
|
|
2205
|
+
${trimmed.slice(
|
|
2206
|
+
0,
|
|
2207
|
+
MAX_CONTEXT_CHARS
|
|
2208
|
+
)}` : `The user's dashboard is titled "${title}". The symbols on screen right now are: ${symbols.size ? [...symbols].join(", ") : "no specific symbols"}.`;
|
|
2209
|
+
const trimmedCatalogue = catalogue?.trim();
|
|
2210
|
+
const frameCatalogue = trimmedCatalogue ? `The frames a user can add in zframes (name \u2014 what it shows), by family:
|
|
2211
|
+
${trimmedCatalogue}
|
|
2212
|
+
|
|
2213
|
+
` : "";
|
|
2214
|
+
const recent = (history ?? []).filter((m) => m.text.trim()).slice(-MAX_HISTORY_TURNS);
|
|
2215
|
+
const transcript = recent.length ? `Conversation so far (most recent last):
|
|
2216
|
+
${recent.map(
|
|
2217
|
+
(m) => `${m.role === "user" ? "User" : "zAI"}: ${m.text.replace(/\s+/g, " ").trim().slice(0, MAX_HISTORY_CHARS)}`
|
|
2218
|
+
).join("\n")}
|
|
2219
|
+
|
|
2220
|
+
` : "";
|
|
2221
|
+
return (
|
|
2222
|
+
// Primer: brief the runner on what zframes is and how it works, so a
|
|
2223
|
+
// general-purpose agent answers as the embedded assistant rather than from
|
|
2224
|
+
// cold. Kept tight — the catalogue + live digest below carry the specifics.
|
|
2225
|
+
`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 perps \u2014 equities (xyz:TSLA), indices (xyz:SP500), commodities (xyz:GOLD), FX (xyz:EUR) \u2014 shown alongside crypto. Help the user read what's on their screen and the markets it tracks.
|
|
1402
2226
|
|
|
1403
|
-
|
|
2227
|
+
${frameCatalogue}${grounding}
|
|
2228
|
+
|
|
2229
|
+
${transcript}Stay strictly on topic. Only answer questions about zframes itself (its frames, data, dashboard, and how to use it) and about markets and finance \u2014 stocks, crypto, prices, funding, market caps, on-chain and macro data, and the symbols on screen. If the question is about anything else (general knowledge, coding, personal advice, writing, other software, etc.), do not answer it: reply in one sentence that you only cover zframes and the markets it tracks, and invite a market or dashboard question instead.
|
|
2230
|
+
|
|
2231
|
+
Answer the user's question in 2\u20134 sentences of plain text \u2014 no markdown headings, no preamble, no tool use, just the answer.
|
|
2232
|
+
|
|
2233
|
+
Question: ${question}`
|
|
2234
|
+
);
|
|
1404
2235
|
}
|
|
1405
2236
|
var askCounter = 0;
|
|
1406
|
-
function runAgent(runner, prompt, cwd) {
|
|
2237
|
+
function runAgent(runner, prompt, cwd, onDelta) {
|
|
1407
2238
|
const outFile = join2(
|
|
1408
2239
|
tmpdir(),
|
|
1409
2240
|
`zframes-ask-${process.pid}-${++askCounter}.txt`
|
|
1410
2241
|
);
|
|
1411
|
-
return new Promise((
|
|
2242
|
+
return new Promise((resolve5) => {
|
|
1412
2243
|
let child;
|
|
1413
2244
|
try {
|
|
1414
|
-
child = spawn(runner.bin, runner.buildArgs(prompt, outFile), {
|
|
2245
|
+
child = spawn(runner.bin, runner.buildArgs(prompt, outFile), {
|
|
2246
|
+
cwd,
|
|
2247
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2248
|
+
});
|
|
1415
2249
|
} catch (error) {
|
|
1416
|
-
|
|
2250
|
+
resolve5({ ok: false, error: String(error) });
|
|
1417
2251
|
return;
|
|
1418
2252
|
}
|
|
1419
2253
|
let stdout = "";
|
|
1420
2254
|
let stderr = "";
|
|
2255
|
+
let lineBuf = "";
|
|
1421
2256
|
let settled = false;
|
|
1422
2257
|
const finish = (r) => {
|
|
1423
2258
|
if (settled) return;
|
|
1424
2259
|
settled = true;
|
|
1425
2260
|
clearTimeout(timer);
|
|
1426
|
-
|
|
2261
|
+
resolve5(r);
|
|
1427
2262
|
};
|
|
1428
2263
|
const timer = setTimeout(() => {
|
|
1429
2264
|
child.kill("SIGKILL");
|
|
1430
2265
|
finish({ ok: false, error: `${runner.label} timed out` });
|
|
1431
2266
|
}, RUN_TIMEOUT_MS);
|
|
1432
|
-
|
|
2267
|
+
const streaming = Boolean(onDelta && runner.parseDelta);
|
|
2268
|
+
const emit = (line) => {
|
|
2269
|
+
const delta = runner.parseDelta?.(line);
|
|
2270
|
+
if (delta) onDelta?.(delta);
|
|
2271
|
+
};
|
|
2272
|
+
child.stdout?.on("data", (d) => {
|
|
2273
|
+
stdout += d;
|
|
2274
|
+
if (!streaming) return;
|
|
2275
|
+
lineBuf += d;
|
|
2276
|
+
let nl;
|
|
2277
|
+
while ((nl = lineBuf.indexOf("\n")) !== -1) {
|
|
2278
|
+
emit(lineBuf.slice(0, nl));
|
|
2279
|
+
lineBuf = lineBuf.slice(nl + 1);
|
|
2280
|
+
}
|
|
2281
|
+
});
|
|
1433
2282
|
child.stderr?.on("data", (d) => stderr += d);
|
|
1434
2283
|
child.on(
|
|
1435
2284
|
"error",
|
|
1436
2285
|
(e) => finish({ ok: false, error: String(e.message) })
|
|
1437
2286
|
);
|
|
1438
2287
|
child.on("close", (code) => {
|
|
2288
|
+
if (streaming && lineBuf) emit(lineBuf);
|
|
1439
2289
|
if (code !== 0) {
|
|
1440
2290
|
finish({
|
|
1441
2291
|
ok: false,
|
|
@@ -1460,7 +2310,7 @@ async function handleAgents(res) {
|
|
|
1460
2310
|
JSON.stringify({ agents: agents.map(({ id, label }) => ({ id, label })) })
|
|
1461
2311
|
);
|
|
1462
2312
|
}
|
|
1463
|
-
function handleAsk(req, res, specFile) {
|
|
2313
|
+
function handleAsk(req, res, specFile, catalogue) {
|
|
1464
2314
|
if (req.method !== "POST") {
|
|
1465
2315
|
res.statusCode = 405;
|
|
1466
2316
|
res.end();
|
|
@@ -1485,44 +2335,295 @@ function handleAsk(req, res, specFile) {
|
|
|
1485
2335
|
});
|
|
1486
2336
|
req.on("end", async () => {
|
|
1487
2337
|
if (aborted) return;
|
|
1488
|
-
const
|
|
2338
|
+
const replyJson = (status, payload) => {
|
|
1489
2339
|
res.statusCode = status;
|
|
1490
2340
|
res.setHeader("content-type", "application/json");
|
|
1491
2341
|
res.end(JSON.stringify(payload));
|
|
1492
2342
|
};
|
|
1493
2343
|
let question;
|
|
1494
2344
|
let requested;
|
|
2345
|
+
let clientContext;
|
|
2346
|
+
let history;
|
|
1495
2347
|
try {
|
|
1496
2348
|
const parsed = JSON.parse(body);
|
|
1497
2349
|
if (typeof parsed.question !== "string" || !parsed.question.trim())
|
|
1498
2350
|
throw new Error("missing question");
|
|
1499
2351
|
question = parsed.question.trim();
|
|
1500
2352
|
requested = typeof parsed.agent === "string" ? parsed.agent : void 0;
|
|
2353
|
+
clientContext = typeof parsed.context === "string" ? parsed.context : void 0;
|
|
2354
|
+
if (Array.isArray(parsed.history))
|
|
2355
|
+
history = parsed.history.filter(
|
|
2356
|
+
(m) => !!m && typeof m === "object" && (m.role === "user" || m.role === "zai") && typeof m.text === "string"
|
|
2357
|
+
).slice(-MAX_HISTORY_TURNS);
|
|
1501
2358
|
} catch (error) {
|
|
1502
|
-
|
|
2359
|
+
replyJson(400, { ok: false, error: String(error.message) });
|
|
1503
2360
|
return;
|
|
1504
2361
|
}
|
|
1505
2362
|
const agents = await detectAgents();
|
|
1506
2363
|
if (agents.length === 0) {
|
|
1507
|
-
|
|
2364
|
+
replyJson(503, {
|
|
1508
2365
|
ok: false,
|
|
1509
2366
|
error: "no agent CLI found \u2014 install claude, codex, or kimi"
|
|
1510
2367
|
});
|
|
1511
2368
|
return;
|
|
1512
2369
|
}
|
|
1513
2370
|
const runner = agents.find((a) => a.id === requested) ?? agents[0];
|
|
1514
|
-
const prompt = await buildPrompt(
|
|
1515
|
-
|
|
2371
|
+
const prompt = await buildPrompt(
|
|
2372
|
+
specFile,
|
|
2373
|
+
question,
|
|
2374
|
+
clientContext,
|
|
2375
|
+
catalogue,
|
|
2376
|
+
history
|
|
2377
|
+
);
|
|
2378
|
+
res.statusCode = 200;
|
|
2379
|
+
res.setHeader("content-type", "application/x-ndjson; charset=utf-8");
|
|
2380
|
+
res.setHeader("cache-control", "no-store");
|
|
2381
|
+
res.setHeader("x-accel-buffering", "no");
|
|
2382
|
+
const send = (event) => {
|
|
2383
|
+
try {
|
|
2384
|
+
res.write(`${JSON.stringify(event)}
|
|
2385
|
+
`);
|
|
2386
|
+
} catch {
|
|
2387
|
+
}
|
|
2388
|
+
};
|
|
2389
|
+
const result = await runAgent(
|
|
2390
|
+
runner,
|
|
2391
|
+
prompt,
|
|
2392
|
+
dirname2(specFile),
|
|
2393
|
+
(text) => send({ type: "delta", text })
|
|
2394
|
+
);
|
|
1516
2395
|
if (result.ok)
|
|
1517
|
-
|
|
1518
|
-
else
|
|
2396
|
+
send({ type: "done", agent: runner.id, answer: result.answer });
|
|
2397
|
+
else send({ type: "error", agent: runner.id, error: result.error });
|
|
2398
|
+
try {
|
|
2399
|
+
res.end();
|
|
2400
|
+
} catch {
|
|
2401
|
+
}
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
// ../core/src/account.ts
|
|
2406
|
+
import { createHmac } from "crypto";
|
|
2407
|
+
import { chmod, mkdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
2408
|
+
function json(res, status, body) {
|
|
2409
|
+
res.statusCode = status;
|
|
2410
|
+
res.setHeader("content-type", "application/json");
|
|
2411
|
+
res.setHeader("cache-control", "no-store");
|
|
2412
|
+
res.end(JSON.stringify(body));
|
|
2413
|
+
}
|
|
2414
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
2415
|
+
function isLocalRequest(req) {
|
|
2416
|
+
const hostname = String(req.headers.host ?? "").split(":")[0];
|
|
2417
|
+
if (!LOCAL_HOSTS.has(hostname)) return false;
|
|
2418
|
+
const origin = req.headers.origin;
|
|
2419
|
+
if (origin !== void 0) {
|
|
2420
|
+
try {
|
|
2421
|
+
if (!LOCAL_HOSTS.has(new URL(String(origin)).hostname)) return false;
|
|
2422
|
+
} catch {
|
|
2423
|
+
return false;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
return true;
|
|
2427
|
+
}
|
|
2428
|
+
var MAX_BODY_BYTES3 = 8192;
|
|
2429
|
+
function readBody(req, res) {
|
|
2430
|
+
return new Promise((resolveBody) => {
|
|
2431
|
+
let body = "";
|
|
2432
|
+
let aborted = false;
|
|
2433
|
+
req.on("data", (chunk) => {
|
|
2434
|
+
if (aborted) return;
|
|
2435
|
+
body += chunk;
|
|
2436
|
+
if (body.length > MAX_BODY_BYTES3) {
|
|
2437
|
+
aborted = true;
|
|
2438
|
+
json(res, 413, { ok: false, error: "body too large" });
|
|
2439
|
+
req.destroy();
|
|
2440
|
+
resolveBody(null);
|
|
2441
|
+
}
|
|
2442
|
+
});
|
|
2443
|
+
req.on("end", () => {
|
|
2444
|
+
if (!aborted) resolveBody(body);
|
|
2445
|
+
});
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
async function readStore() {
|
|
2449
|
+
try {
|
|
2450
|
+
const parsed = JSON.parse(
|
|
2451
|
+
await readFile3(credentialsFile(), "utf8")
|
|
2452
|
+
);
|
|
2453
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
2454
|
+
} catch {
|
|
2455
|
+
return {};
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
async function writeStore(store) {
|
|
2459
|
+
await mkdir(storeHome(), { recursive: true, mode: 448 });
|
|
2460
|
+
await chmod(storeHome(), 448).catch(() => {
|
|
2461
|
+
});
|
|
2462
|
+
await writeFile2(credentialsFile(), `${JSON.stringify(store, null, 2)}
|
|
2463
|
+
`, {
|
|
2464
|
+
encoding: "utf8",
|
|
2465
|
+
mode: 384
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
function envCredential(source) {
|
|
2469
|
+
const up = source.toUpperCase();
|
|
2470
|
+
const key = process.env[`ZFRAMES_${up}_KEY`];
|
|
2471
|
+
const secret = process.env[`ZFRAMES_${up}_SECRET`];
|
|
2472
|
+
return key && secret ? { key, secret } : null;
|
|
2473
|
+
}
|
|
2474
|
+
async function getCredential(source) {
|
|
2475
|
+
const store = await readStore();
|
|
2476
|
+
return store[source] ?? envCredential(source);
|
|
2477
|
+
}
|
|
2478
|
+
function maskKey(key) {
|
|
2479
|
+
return key.length <= 4 ? "\u2026" : `\u2026${key.slice(-4)}`;
|
|
2480
|
+
}
|
|
2481
|
+
var STABLES = /* @__PURE__ */ new Set([
|
|
2482
|
+
"USDT",
|
|
2483
|
+
"USDC",
|
|
2484
|
+
"BUSD",
|
|
2485
|
+
"DAI",
|
|
2486
|
+
"FDUSD",
|
|
2487
|
+
"TUSD",
|
|
2488
|
+
"USDP",
|
|
2489
|
+
"USD"
|
|
2490
|
+
]);
|
|
2491
|
+
var BINANCE_BASE = "https://api.binance.com";
|
|
2492
|
+
var BINANCE_TIMEOUT_MS = 15e3;
|
|
2493
|
+
function signBinance(query, secret) {
|
|
2494
|
+
return createHmac("sha256", secret).update(query).digest("hex");
|
|
2495
|
+
}
|
|
2496
|
+
function binanceHoldings(data) {
|
|
2497
|
+
return (data.balances ?? []).map((b) => ({
|
|
2498
|
+
symbol: b.asset,
|
|
2499
|
+
amount: Number(b.free) + Number(b.locked)
|
|
2500
|
+
})).filter((b) => Number.isFinite(b.amount) && b.amount > 0).map((b) => ({
|
|
2501
|
+
symbol: b.symbol,
|
|
2502
|
+
amount: b.amount,
|
|
2503
|
+
valueUsd: STABLES.has(b.symbol.toUpperCase()) ? b.amount : void 0
|
|
2504
|
+
}));
|
|
2505
|
+
}
|
|
2506
|
+
async function binanceAccount(cred, nowMs) {
|
|
2507
|
+
const query = `timestamp=${nowMs}&recvWindow=5000`;
|
|
2508
|
+
const signature = signBinance(query, cred.secret);
|
|
2509
|
+
const res = await fetch(
|
|
2510
|
+
`${BINANCE_BASE}/api/v3/account?${query}&signature=${signature}`,
|
|
2511
|
+
{
|
|
2512
|
+
headers: { "X-MBX-APIKEY": cred.key },
|
|
2513
|
+
signal: AbortSignal.timeout(BINANCE_TIMEOUT_MS)
|
|
2514
|
+
}
|
|
2515
|
+
);
|
|
2516
|
+
if (!res.ok) {
|
|
2517
|
+
throw new Error(`binance ${res.status}`);
|
|
2518
|
+
}
|
|
2519
|
+
return res.json();
|
|
2520
|
+
}
|
|
2521
|
+
var binanceAdapter = {
|
|
2522
|
+
async verify(cred) {
|
|
2523
|
+
await binanceAccount(cred, Date.now());
|
|
2524
|
+
},
|
|
2525
|
+
async portfolio(cred) {
|
|
2526
|
+
const data = await binanceAccount(cred, Date.now());
|
|
2527
|
+
return {
|
|
2528
|
+
source: "binance",
|
|
2529
|
+
label: "Binance",
|
|
2530
|
+
holdings: binanceHoldings(data),
|
|
2531
|
+
asOf: Date.now()
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
};
|
|
2535
|
+
var ADAPTERS = { binance: binanceAdapter };
|
|
2536
|
+
function sourceParam(req) {
|
|
2537
|
+
const query = (req.url ?? "").split("?")[1] ?? "";
|
|
2538
|
+
return new URLSearchParams(query).get("source") ?? "";
|
|
2539
|
+
}
|
|
2540
|
+
async function handleAccountPortfolio(req, res) {
|
|
2541
|
+
if (!isLocalRequest(req))
|
|
2542
|
+
return json(res, 403, { ok: false, error: "non-local origin" });
|
|
2543
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
2544
|
+
return json(res, 405, { ok: false, error: "GET only" });
|
|
2545
|
+
const source = sourceParam(req);
|
|
2546
|
+
const adapter = ADAPTERS[source];
|
|
2547
|
+
if (!adapter)
|
|
2548
|
+
return json(res, 400, { ok: false, error: `unknown source: ${source}` });
|
|
2549
|
+
const cred = await getCredential(source);
|
|
2550
|
+
if (!cred)
|
|
2551
|
+
return json(res, 401, {
|
|
2552
|
+
ok: false,
|
|
2553
|
+
connected: false,
|
|
2554
|
+
error: "not connected"
|
|
2555
|
+
});
|
|
2556
|
+
try {
|
|
2557
|
+
json(res, 200, await adapter.portfolio(cred));
|
|
2558
|
+
} catch (error) {
|
|
2559
|
+
json(res, 502, { ok: false, error: String(error) });
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
async function handleAccountCredentials(req, res) {
|
|
2563
|
+
if (!isLocalRequest(req))
|
|
2564
|
+
return json(res, 403, { ok: false, error: "non-local origin" });
|
|
2565
|
+
const method = req.method ?? "GET";
|
|
2566
|
+
if (method === "GET") {
|
|
2567
|
+
const cred = await getCredential(sourceParam(req));
|
|
2568
|
+
return json(res, 200, {
|
|
2569
|
+
connected: !!cred,
|
|
2570
|
+
keyMasked: cred ? maskKey(cred.key) : null
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2573
|
+
if (method !== "POST" && method !== "DELETE")
|
|
2574
|
+
return json(res, 405, { ok: false, error: "method not allowed" });
|
|
2575
|
+
if (!String(req.headers["content-type"] ?? "").includes("application/json"))
|
|
2576
|
+
return json(res, 415, {
|
|
2577
|
+
ok: false,
|
|
2578
|
+
error: "content-type must be application/json"
|
|
2579
|
+
});
|
|
2580
|
+
const raw = await readBody(req, res);
|
|
2581
|
+
if (raw === null) return;
|
|
2582
|
+
let parsed;
|
|
2583
|
+
try {
|
|
2584
|
+
parsed = JSON.parse(raw);
|
|
2585
|
+
} catch {
|
|
2586
|
+
return json(res, 400, { ok: false, error: "invalid json" });
|
|
2587
|
+
}
|
|
2588
|
+
const source = String(parsed.source ?? "");
|
|
2589
|
+
if (!ADAPTERS[source])
|
|
2590
|
+
return json(res, 400, { ok: false, error: `unknown source: ${source}` });
|
|
2591
|
+
if (method === "DELETE") {
|
|
2592
|
+
const store2 = await readStore();
|
|
2593
|
+
delete store2[source];
|
|
2594
|
+
await writeStore(store2);
|
|
2595
|
+
return json(res, 200, { ok: true, connected: false });
|
|
2596
|
+
}
|
|
2597
|
+
const key = String(parsed.key ?? "").trim();
|
|
2598
|
+
const secret = String(parsed.secret ?? "").trim();
|
|
2599
|
+
if (!key || !secret)
|
|
2600
|
+
return json(res, 400, { ok: false, error: "key and secret required" });
|
|
2601
|
+
try {
|
|
2602
|
+
await ADAPTERS[source].verify({ key, secret });
|
|
2603
|
+
} catch (error) {
|
|
2604
|
+
return json(res, 400, {
|
|
2605
|
+
ok: false,
|
|
2606
|
+
verified: false,
|
|
2607
|
+
error: `verification failed: ${String(error)}`
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
const store = await readStore();
|
|
2611
|
+
store[source] = { key, secret };
|
|
2612
|
+
await writeStore(store);
|
|
2613
|
+
return json(res, 200, {
|
|
2614
|
+
ok: true,
|
|
2615
|
+
verified: true,
|
|
2616
|
+
connected: true,
|
|
2617
|
+
keyMasked: maskKey(key)
|
|
1519
2618
|
});
|
|
1520
2619
|
}
|
|
1521
2620
|
|
|
1522
2621
|
// src/serve.ts
|
|
1523
2622
|
var DEFAULT_PORT = 37263;
|
|
2623
|
+
var MAX_SWITCH_BODY_BYTES = 1e5;
|
|
2624
|
+
var FRAME_CATALOGUE = catalogueSummary(frameMetas);
|
|
1524
2625
|
function parseArgs2(args) {
|
|
1525
|
-
let file
|
|
2626
|
+
let file;
|
|
1526
2627
|
let port = DEFAULT_PORT;
|
|
1527
2628
|
let contact = process.env.ZFRAMES_CONTACT;
|
|
1528
2629
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -1546,31 +2647,97 @@ function parseArgs2(args) {
|
|
|
1546
2647
|
}
|
|
1547
2648
|
return { file, port, contact };
|
|
1548
2649
|
}
|
|
2650
|
+
function sendJson(res, status, body) {
|
|
2651
|
+
res.statusCode = status;
|
|
2652
|
+
res.setHeader("content-type", "application/json");
|
|
2653
|
+
res.setHeader("cache-control", "no-store");
|
|
2654
|
+
res.end(JSON.stringify(body));
|
|
2655
|
+
}
|
|
1549
2656
|
function serve(args) {
|
|
1550
2657
|
const parsed = parseArgs2(args);
|
|
1551
2658
|
if ("error" in parsed) {
|
|
1552
2659
|
console.error(`\u2717 ${parsed.error}`);
|
|
1553
2660
|
console.error(
|
|
1554
|
-
"usage: zframes serve [dashboard.json] [--port <n>] [--contact <email>]"
|
|
2661
|
+
"usage: zframes serve [name|dashboard.json] [--port <n>] [--contact <email>]"
|
|
1555
2662
|
);
|
|
1556
2663
|
return Promise.resolve(1);
|
|
1557
2664
|
}
|
|
1558
|
-
const
|
|
1559
|
-
if (
|
|
1560
|
-
console.error(`\u2717
|
|
1561
|
-
console.error(" pass a path, or run from a directory that has one.");
|
|
2665
|
+
const target = resolveServeTarget(parsed.file, process.cwd());
|
|
2666
|
+
if ("error" in target) {
|
|
2667
|
+
console.error(`\u2717 ${target.error}`);
|
|
1562
2668
|
return Promise.resolve(1);
|
|
1563
2669
|
}
|
|
1564
|
-
|
|
2670
|
+
let current = target;
|
|
2671
|
+
const canSwitch = target.kind === "store";
|
|
1565
2672
|
const bundleDir = fileURLToPath(new URL("../runtime", import.meta.url));
|
|
1566
|
-
if (!
|
|
2673
|
+
if (!existsSync3(join3(bundleDir, "index.html"))) {
|
|
1567
2674
|
console.error(`\u2717 runtime bundle missing at ${bundleDir}`);
|
|
1568
2675
|
console.error(" run `pnpm build:cli` to build it.");
|
|
1569
2676
|
return Promise.resolve(1);
|
|
1570
2677
|
}
|
|
1571
2678
|
const serveBundle = sirv(bundleDir, { dev: true });
|
|
1572
|
-
|
|
2679
|
+
let serveSiblings = sirv(resolve3(current.file, ".."), { dev: true });
|
|
1573
2680
|
const serveSpa = sirv(bundleDir, { dev: true, single: true });
|
|
2681
|
+
function handleSwitch(req, res) {
|
|
2682
|
+
if (!isLocalRequest(req)) {
|
|
2683
|
+
sendJson(res, 403, { ok: false, error: "loopback only" });
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
if (req.method !== "POST") {
|
|
2687
|
+
res.statusCode = 405;
|
|
2688
|
+
res.end();
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
if (!canSwitch) {
|
|
2692
|
+
sendJson(res, 409, {
|
|
2693
|
+
ok: false,
|
|
2694
|
+
error: "switching is only available when serving from the store"
|
|
2695
|
+
});
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
if (!String(req.headers["content-type"] ?? "").includes("application/json")) {
|
|
2699
|
+
res.statusCode = 415;
|
|
2700
|
+
res.end();
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
let body = "";
|
|
2704
|
+
let aborted = false;
|
|
2705
|
+
req.on("data", (chunk) => {
|
|
2706
|
+
if (aborted) return;
|
|
2707
|
+
body += chunk;
|
|
2708
|
+
if (body.length > MAX_SWITCH_BODY_BYTES) {
|
|
2709
|
+
aborted = true;
|
|
2710
|
+
res.statusCode = 413;
|
|
2711
|
+
res.end();
|
|
2712
|
+
req.destroy();
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
2715
|
+
req.on("end", () => {
|
|
2716
|
+
if (aborted) return;
|
|
2717
|
+
let name;
|
|
2718
|
+
try {
|
|
2719
|
+
name = JSON.parse(body || "{}").name;
|
|
2720
|
+
} catch {
|
|
2721
|
+
sendJson(res, 400, { ok: false, error: "invalid JSON body" });
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
if (typeof name !== "string" || !isValidName(name)) {
|
|
2725
|
+
sendJson(res, 400, { ok: false, error: "invalid dashboard name" });
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
const file = findDashboardFile(name);
|
|
2729
|
+
if (!file) {
|
|
2730
|
+
sendJson(res, 404, {
|
|
2731
|
+
ok: false,
|
|
2732
|
+
error: `no dashboard named "${name}" in the store`
|
|
2733
|
+
});
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
current = { kind: "store", name, file };
|
|
2737
|
+
serveSiblings = sirv(resolve3(current.file, ".."), { dev: true });
|
|
2738
|
+
sendJson(res, 200, { ok: true, name });
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
1574
2741
|
return new Promise((done) => {
|
|
1575
2742
|
const server = createServer((req, res) => {
|
|
1576
2743
|
const rawPath = (req.url ?? "/").split("?")[0];
|
|
@@ -1584,7 +2751,7 @@ function serve(args) {
|
|
|
1584
2751
|
}
|
|
1585
2752
|
if (path === DASHBOARD_READ_ROUTE) {
|
|
1586
2753
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
1587
|
-
void handleSpecRead(file, res);
|
|
2754
|
+
void handleSpecRead(current.file, res);
|
|
1588
2755
|
} else {
|
|
1589
2756
|
res.statusCode = 405;
|
|
1590
2757
|
res.end();
|
|
@@ -1592,7 +2759,32 @@ function serve(args) {
|
|
|
1592
2759
|
return;
|
|
1593
2760
|
}
|
|
1594
2761
|
if (path === DASHBOARD_WRITE_ROUTE) {
|
|
1595
|
-
handleSpecWrite(req, res, file);
|
|
2762
|
+
handleSpecWrite(req, res, current.file);
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
if (path === DASHBOARD_LIST_ROUTE) {
|
|
2766
|
+
if (!isLocalRequest(req)) {
|
|
2767
|
+
sendJson(res, 403, { ok: false, error: "loopback only" });
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
if (req.method === "GET" || req.method === "HEAD") {
|
|
2771
|
+
sendJson(res, 200, {
|
|
2772
|
+
current: current.kind === "store" ? current.name : null,
|
|
2773
|
+
canSwitch,
|
|
2774
|
+
dashboards: canSwitch ? listDashboards().map((e) => ({
|
|
2775
|
+
name: e.name,
|
|
2776
|
+
title: e.title,
|
|
2777
|
+
isDefault: e.isDefault
|
|
2778
|
+
})) : []
|
|
2779
|
+
});
|
|
2780
|
+
} else {
|
|
2781
|
+
res.statusCode = 405;
|
|
2782
|
+
res.end();
|
|
2783
|
+
}
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
if (path === DASHBOARD_SWITCH_ROUTE) {
|
|
2787
|
+
handleSwitch(req, res);
|
|
1596
2788
|
return;
|
|
1597
2789
|
}
|
|
1598
2790
|
if (path === AGENTS_LIST_ROUTE) {
|
|
@@ -1605,7 +2797,15 @@ function serve(args) {
|
|
|
1605
2797
|
return;
|
|
1606
2798
|
}
|
|
1607
2799
|
if (path === ASK_ROUTE) {
|
|
1608
|
-
handleAsk(req, res, file);
|
|
2800
|
+
handleAsk(req, res, current.file, FRAME_CATALOGUE);
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
if (path === ACCOUNT_PORTFOLIO_ROUTE) {
|
|
2804
|
+
void handleAccountPortfolio(req, res);
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
if (path === ACCOUNT_CREDENTIALS_ROUTE) {
|
|
2808
|
+
void handleAccountCredentials(req, res);
|
|
1609
2809
|
return;
|
|
1610
2810
|
}
|
|
1611
2811
|
if (path === DASHBOARD_PROXY_ROUTE) {
|
|
@@ -1649,17 +2849,23 @@ function serve(args) {
|
|
|
1649
2849
|
});
|
|
1650
2850
|
server.listen(parsed.port, "127.0.0.1", () => {
|
|
1651
2851
|
const url = `http://localhost:${parsed.port}`;
|
|
2852
|
+
const label = target.kind === "store" ? `"${target.name}" from your store` : target.file;
|
|
1652
2853
|
console.log(`\u26A1 zframes is live at ${url}`);
|
|
1653
2854
|
console.log(
|
|
1654
|
-
` serving ${
|
|
2855
|
+
` serving ${label} \u2014 live editing on; drag, resize, then Save writes back.`
|
|
1655
2856
|
);
|
|
2857
|
+
if (canSwitch && listDashboards().length > 1) {
|
|
2858
|
+
console.log(
|
|
2859
|
+
" switch dashboards from the header dropdown in the app."
|
|
2860
|
+
);
|
|
2861
|
+
}
|
|
1656
2862
|
});
|
|
1657
2863
|
});
|
|
1658
2864
|
}
|
|
1659
2865
|
|
|
1660
2866
|
// src/snapshot.ts
|
|
1661
|
-
import { existsSync as
|
|
1662
|
-
import { dirname as dirname3, resolve as
|
|
2867
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
2868
|
+
import { dirname as dirname3, join as join4, resolve as resolve4 } from "path";
|
|
1663
2869
|
|
|
1664
2870
|
// ../core/src/cache.ts
|
|
1665
2871
|
var TtlCache = class {
|
|
@@ -2168,19 +3374,19 @@ var DAY_MS = 864e5;
|
|
|
2168
3374
|
function loadSpec(file) {
|
|
2169
3375
|
let raw;
|
|
2170
3376
|
try {
|
|
2171
|
-
raw =
|
|
3377
|
+
raw = readFileSync2(file, "utf8");
|
|
2172
3378
|
} catch {
|
|
2173
3379
|
console.error(`\u2717 cannot read ${file}`);
|
|
2174
3380
|
return null;
|
|
2175
3381
|
}
|
|
2176
|
-
let
|
|
3382
|
+
let json2;
|
|
2177
3383
|
try {
|
|
2178
|
-
|
|
3384
|
+
json2 = JSON.parse(raw);
|
|
2179
3385
|
} catch (error) {
|
|
2180
3386
|
console.error(`\u2717 ${file} is not valid JSON: ${error.message}`);
|
|
2181
3387
|
return null;
|
|
2182
3388
|
}
|
|
2183
|
-
const parsed = DashboardSpecSchema.safeParse(
|
|
3389
|
+
const parsed = DashboardSpecSchema.safeParse(json2);
|
|
2184
3390
|
if (!parsed.success) {
|
|
2185
3391
|
console.error(`\u2717 ${file} is not a valid dashboard spec:`);
|
|
2186
3392
|
for (const issue of parsed.error.issues)
|
|
@@ -2223,6 +3429,24 @@ function flagValue(args, name) {
|
|
|
2223
3429
|
const index = args.indexOf(name);
|
|
2224
3430
|
return index >= 0 ? args[index + 1] : void 0;
|
|
2225
3431
|
}
|
|
3432
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
3433
|
+
"--log",
|
|
3434
|
+
"--date",
|
|
3435
|
+
"--model",
|
|
3436
|
+
"--effort",
|
|
3437
|
+
"--config"
|
|
3438
|
+
]);
|
|
3439
|
+
function positionalArg(args) {
|
|
3440
|
+
for (let i = 0; i < args.length; i++) {
|
|
3441
|
+
const a = args[i];
|
|
3442
|
+
if (a.startsWith("-")) {
|
|
3443
|
+
if (VALUE_FLAGS.has(a)) i++;
|
|
3444
|
+
continue;
|
|
3445
|
+
}
|
|
3446
|
+
return a;
|
|
3447
|
+
}
|
|
3448
|
+
return void 0;
|
|
3449
|
+
}
|
|
2226
3450
|
function runMeta(args) {
|
|
2227
3451
|
const pick = (flag, env) => flagValue(args, flag) ?? process.env[env] ?? null;
|
|
2228
3452
|
const rawConfig = pick("--config", "ZFRAMES_CONFIG");
|
|
@@ -2241,27 +3465,31 @@ function runMeta(args) {
|
|
|
2241
3465
|
config
|
|
2242
3466
|
};
|
|
2243
3467
|
}
|
|
2244
|
-
function
|
|
2245
|
-
|
|
2246
|
-
|
|
3468
|
+
function defaultLogPath(target) {
|
|
3469
|
+
return target.kind === "store" ? join4(dirname3(target.file), "daily-analysis.json") : resolve4(dirname3(target.file), "..", "public", "daily-analysis.json");
|
|
3470
|
+
}
|
|
3471
|
+
function loadPriorEntry(logPath) {
|
|
3472
|
+
if (!existsSync4(logPath)) return null;
|
|
2247
3473
|
try {
|
|
2248
|
-
const
|
|
2249
|
-
const entries =
|
|
3474
|
+
const json2 = JSON.parse(readFileSync2(logPath, "utf8"));
|
|
3475
|
+
const entries = json2.entries ?? [];
|
|
2250
3476
|
return entries.length ? entries[entries.length - 1] : null;
|
|
2251
3477
|
} catch {
|
|
2252
3478
|
return null;
|
|
2253
3479
|
}
|
|
2254
3480
|
}
|
|
3481
|
+
var USAGE = "usage: zframes snapshot [name|dashboard.json] [--log <file>] [--date YYYY-MM-DD]\n [--model <id>] [--effort <level>] [--config <json>]\n (no target \u2192 the default store dashboard, just like `serve`;\n or ZFRAMES_MODEL / ZFRAMES_EFFORT / ZFRAMES_CONFIG env)";
|
|
2255
3482
|
async function snapshot(args) {
|
|
2256
|
-
const
|
|
2257
|
-
if (
|
|
2258
|
-
console.error(
|
|
2259
|
-
|
|
2260
|
-
);
|
|
3483
|
+
const resolved = resolveServeTarget(positionalArg(args), process.cwd());
|
|
3484
|
+
if ("error" in resolved) {
|
|
3485
|
+
console.error(`\u2717 ${resolved.error}`);
|
|
3486
|
+
console.error(USAGE);
|
|
2261
3487
|
return 1;
|
|
2262
3488
|
}
|
|
3489
|
+
const file = resolved.file;
|
|
2263
3490
|
const logFlag = flagValue(args, "--log");
|
|
2264
3491
|
const dateFlag = flagValue(args, "--date");
|
|
3492
|
+
const logPath = logFlag ? resolve4(logFlag) : defaultLogPath(resolved);
|
|
2265
3493
|
const spec = loadSpec(file);
|
|
2266
3494
|
if (!spec) return 1;
|
|
2267
3495
|
const universe = symbolsFromSpec(spec);
|
|
@@ -2286,6 +3514,16 @@ async function snapshot(args) {
|
|
|
2286
3514
|
const out = {
|
|
2287
3515
|
date: dateFlag ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
2288
3516
|
run: runMeta(args),
|
|
3517
|
+
// Which dashboard this snapshot is for, and where its brief lives — so the
|
|
3518
|
+
// /zframes-brief agent writes the new entry to the RIGHT per-dashboard log
|
|
3519
|
+
// (`logPath`, a sibling of this dashboard inside its folder). The frame reads
|
|
3520
|
+
// it at its default `/daily-analysis.json` — no per-name `src` needed.
|
|
3521
|
+
dashboard: {
|
|
3522
|
+
kind: resolved.kind,
|
|
3523
|
+
name: resolved.kind === "store" ? resolved.name : null,
|
|
3524
|
+
file,
|
|
3525
|
+
logPath
|
|
3526
|
+
},
|
|
2289
3527
|
universe,
|
|
2290
3528
|
featured,
|
|
2291
3529
|
market: {
|
|
@@ -2297,7 +3535,7 @@ async function snapshot(args) {
|
|
|
2297
3535
|
global,
|
|
2298
3536
|
tvl: tvl ? tvl.slice(0, 12) : null
|
|
2299
3537
|
},
|
|
2300
|
-
priorEntry: loadPriorEntry(
|
|
3538
|
+
priorEntry: loadPriorEntry(logPath)
|
|
2301
3539
|
};
|
|
2302
3540
|
console.log(JSON.stringify(out, null, 2));
|
|
2303
3541
|
return 0;
|
|
@@ -2306,39 +3544,57 @@ async function snapshot(args) {
|
|
|
2306
3544
|
// src/index.ts
|
|
2307
3545
|
var HELP = `zframes \u2014 AI-personalizable market dashboards
|
|
2308
3546
|
|
|
3547
|
+
A dashboard is one dashboard.json. Point at a file, or name one and it lives in
|
|
3548
|
+
your global store ($XDG_CONFIG_HOME/zframes, default ~/.config/zframes) so you
|
|
3549
|
+
can run from anywhere and keep many. A bare token (e.g. \`crypto\`) is a store
|
|
3550
|
+
name; anything with a "/" or a ".json" suffix is a file path.
|
|
3551
|
+
|
|
2309
3552
|
usage:
|
|
2310
|
-
zframes init [dir|file]
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
zframes
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
3553
|
+
zframes init [name|dir|file] write a bare, valid dashboard (envelope only \u2014
|
|
3554
|
+
version, author, grid, background, theme, empty
|
|
3555
|
+
frames) for the agent to fill in. A name lands in
|
|
3556
|
+
the store (and becomes the default if you don't
|
|
3557
|
+
have one yet); --title <t>, --author <a>,
|
|
3558
|
+
--default, --force
|
|
3559
|
+
zframes serve [name|file] serve a dashboard as a live, editable terminal at
|
|
3560
|
+
localhost:37263 (--port <n>). With no arg: the
|
|
3561
|
+
default store dashboard, else ./dashboard.json,
|
|
3562
|
+
else a sole store entry. Save writes back; switch
|
|
3563
|
+
store dashboards from the in-app header dropdown
|
|
3564
|
+
zframes list list the dashboards in your store (default *)
|
|
3565
|
+
zframes use <name> set the default store dashboard
|
|
3566
|
+
zframes catalogue print the frame catalogue as JSON Schema
|
|
3567
|
+
(this is what a generating agent reads)
|
|
3568
|
+
zframes lint <name|file> validate a dashboard; exit 1 with readable errors
|
|
3569
|
+
(the agent's self-correction feedback)
|
|
3570
|
+
zframes snapshot [name|file] gather a keyless market snapshot for the symbols
|
|
3571
|
+
on the dashboard + the prior brief, as JSON on
|
|
3572
|
+
stdout (the deterministic half of /zframes-brief;
|
|
3573
|
+
no target \u2192 the default store dashboard)
|
|
3574
|
+
zframes help this text
|
|
2325
3575
|
`;
|
|
2326
|
-
function lint(
|
|
3576
|
+
function lint(arg) {
|
|
3577
|
+
const resolved = classifyTarget(arg, process.cwd());
|
|
3578
|
+
if ("error" in resolved) {
|
|
3579
|
+
console.error(`\u2717 ${resolved.error}`);
|
|
3580
|
+
return 1;
|
|
3581
|
+
}
|
|
3582
|
+
const file = resolved.file;
|
|
2327
3583
|
let raw;
|
|
2328
3584
|
try {
|
|
2329
|
-
raw =
|
|
3585
|
+
raw = readFileSync3(file, "utf8");
|
|
2330
3586
|
} catch {
|
|
2331
3587
|
console.error(`\u2717 cannot read ${file}`);
|
|
2332
3588
|
return 1;
|
|
2333
3589
|
}
|
|
2334
|
-
let
|
|
3590
|
+
let json2;
|
|
2335
3591
|
try {
|
|
2336
|
-
|
|
3592
|
+
json2 = JSON.parse(raw);
|
|
2337
3593
|
} catch (error) {
|
|
2338
3594
|
console.error(`\u2717 ${file} is not valid JSON: ${error.message}`);
|
|
2339
3595
|
return 1;
|
|
2340
3596
|
}
|
|
2341
|
-
const parsed = DashboardSpecSchema.safeParse(
|
|
3597
|
+
const parsed = DashboardSpecSchema.safeParse(json2);
|
|
2342
3598
|
if (!parsed.success) {
|
|
2343
3599
|
console.error(`\u2717 ${file} is not a valid dashboard spec:`);
|
|
2344
3600
|
for (const issue of parsed.error.issues)
|
|
@@ -2374,6 +3630,11 @@ async function main() {
|
|
|
2374
3630
|
return lint(arg);
|
|
2375
3631
|
case "serve":
|
|
2376
3632
|
return serve(args.slice(1));
|
|
3633
|
+
case "list":
|
|
3634
|
+
case "ls":
|
|
3635
|
+
return list();
|
|
3636
|
+
case "use":
|
|
3637
|
+
return use(args.slice(1));
|
|
2377
3638
|
case "snapshot":
|
|
2378
3639
|
return snapshot(args.slice(1));
|
|
2379
3640
|
case "help":
|