zframes 0.3.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 +533 -84
- package/package.json +5 -5
- 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/assets/editor-DVTcqJo4.js +0 -68
- package/runtime/assets/editor-DnV0I9s-.css +0 -1
- package/runtime/assets/index-DEGnog6Q.css +0 -1
- package/runtime/assets/index-DPlG_EDx.js +0 -35727
- package/runtime/assets/index-KRy1feFG.js +0 -761
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";
|
|
@@ -66,8 +66,16 @@ var FrameInstanceSchema = z2.object({
|
|
|
66
66
|
)
|
|
67
67
|
});
|
|
68
68
|
var BackgroundSchema = z2.object({
|
|
69
|
-
type: z2.enum(["none", "gradient", "unicorn"]).default("
|
|
70
|
-
"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'."
|
|
71
79
|
),
|
|
72
80
|
projectId: z2.string().optional().describe(
|
|
73
81
|
"Unicorn Studio public project id \u2014 required when type is 'unicorn'."
|
|
@@ -135,11 +143,11 @@ var AppearanceSchema = z2.object({
|
|
|
135
143
|
var DashboardSpecSchema = z2.preprocess(
|
|
136
144
|
(raw) => {
|
|
137
145
|
if (!raw || typeof raw !== "object") return raw;
|
|
138
|
-
|
|
146
|
+
let r = raw;
|
|
139
147
|
const grid = r.grid;
|
|
140
148
|
if (grid && typeof grid === "object" && "radius" in grid) {
|
|
141
149
|
const { radius, ...restGrid } = grid;
|
|
142
|
-
|
|
150
|
+
r = {
|
|
143
151
|
...r,
|
|
144
152
|
grid: restGrid,
|
|
145
153
|
appearance: {
|
|
@@ -148,7 +156,14 @@ var DashboardSpecSchema = z2.preprocess(
|
|
|
148
156
|
}
|
|
149
157
|
};
|
|
150
158
|
}
|
|
151
|
-
|
|
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;
|
|
152
167
|
},
|
|
153
168
|
z2.object({
|
|
154
169
|
version: z2.coerce.string().default("1.0.0").describe(
|
|
@@ -182,7 +197,11 @@ var DashboardSpecSchema = z2.preprocess(
|
|
|
182
197
|
gap: 12
|
|
183
198
|
}),
|
|
184
199
|
background: BackgroundSchema.default({
|
|
185
|
-
type: "
|
|
200
|
+
type: "none",
|
|
201
|
+
color: "#0a0a12",
|
|
202
|
+
gradientFrom: "#1b1e4d",
|
|
203
|
+
gradientTo: "#07070c",
|
|
204
|
+
gradientAngle: 160,
|
|
186
205
|
scale: 1,
|
|
187
206
|
dpi: 1.5,
|
|
188
207
|
opacity: 0.16
|
|
@@ -211,6 +230,164 @@ var DashboardSpecSchema = z2.preprocess(
|
|
|
211
230
|
})
|
|
212
231
|
);
|
|
213
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
|
+
|
|
214
391
|
// ../core/src/frame.ts
|
|
215
392
|
function defineFrameMeta(meta) {
|
|
216
393
|
return meta;
|
|
@@ -244,7 +421,8 @@ var SOURCES = {
|
|
|
244
421
|
},
|
|
245
422
|
mempool: { name: "mempool.space", url: "https://mempool.space" },
|
|
246
423
|
deribit: { name: "Deribit", url: "https://www.deribit.com" },
|
|
247
|
-
coinpaprika: { name: "Coinpaprika", url: "https://coinpaprika.com" }
|
|
424
|
+
coinpaprika: { name: "Coinpaprika", url: "https://coinpaprika.com" },
|
|
425
|
+
frankfurter: { name: "Frankfurter (ECB)", url: "https://frankfurter.dev" }
|
|
248
426
|
};
|
|
249
427
|
var clockMeta = defineFrameMeta({
|
|
250
428
|
name: "clock",
|
|
@@ -331,12 +509,12 @@ var priceChartMeta = defineFrameMeta({
|
|
|
331
509
|
category: "markets",
|
|
332
510
|
iconUrl: widgetIcon("price-chart"),
|
|
333
511
|
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
334
|
-
description: "Live animated price chart (candlestick or line) for one symbol \u2014 canvas-rendered at 60fps via liveline, streaming live off the Hyperliquid WebSocket. Works for HIP-3
|
|
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.",
|
|
335
513
|
capabilities: ["ohlcv", "quote-stream"],
|
|
336
514
|
source: SOURCES.hyperliquid,
|
|
337
515
|
schema: z3.object({
|
|
338
516
|
symbol: z3.string().min(1).describe(
|
|
339
|
-
'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".'
|
|
340
518
|
),
|
|
341
519
|
interval: z3.enum(["1m", "5m", "15m", "1h", "4h", "1d"]).default("1h").describe("Candle interval."),
|
|
342
520
|
mode: z3.enum(["candle", "line"]).default("candle").describe("Candlestick or smooth line rendering."),
|
|
@@ -353,7 +531,7 @@ var priceLivelineMeta = defineFrameMeta({
|
|
|
353
531
|
source: SOURCES.hyperliquid,
|
|
354
532
|
schema: z3.object({
|
|
355
533
|
symbols: z3.array(z3.string()).min(2).max(8).describe(
|
|
356
|
-
'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.'
|
|
357
535
|
),
|
|
358
536
|
windowSec: z3.number().int().min(10).max(300).default(30).describe(
|
|
359
537
|
"Rolling live window in seconds. 30 mirrors the zhive liveline view; use 60\u2013300 for slower dashboards."
|
|
@@ -373,7 +551,7 @@ var priceTickerMeta = defineFrameMeta({
|
|
|
373
551
|
source: SOURCES.hyperliquid,
|
|
374
552
|
schema: z3.object({
|
|
375
553
|
symbols: z3.array(z3.string()).min(1).describe(
|
|
376
|
-
'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".'
|
|
377
555
|
)
|
|
378
556
|
})
|
|
379
557
|
});
|
|
@@ -431,6 +609,24 @@ var ratesBoardMeta = defineFrameMeta({
|
|
|
431
609
|
maxTreasuryRates: z3.number().int().min(1).max(8).default(4).describe("How many Treasury average-rate rows to show.")
|
|
432
610
|
})
|
|
433
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
|
+
});
|
|
434
630
|
var inflationPulseMeta = defineFrameMeta({
|
|
435
631
|
name: "inflation-pulse",
|
|
436
632
|
category: "macro",
|
|
@@ -1418,6 +1614,7 @@ var frameMetas = [
|
|
|
1418
1614
|
priceLivelineMeta,
|
|
1419
1615
|
priceTickerMeta,
|
|
1420
1616
|
ratesBoardMeta,
|
|
1617
|
+
fxBoardMeta,
|
|
1421
1618
|
shortVolumeMeta,
|
|
1422
1619
|
topMoversMeta,
|
|
1423
1620
|
tvlTreemapMeta,
|
|
@@ -1450,15 +1647,87 @@ var frameMetas = [
|
|
|
1450
1647
|
optionsOiStrikeMeta,
|
|
1451
1648
|
coinMoversMeta
|
|
1452
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
|
+
}
|
|
1453
1721
|
|
|
1454
1722
|
// src/init.ts
|
|
1455
|
-
import { existsSync, mkdirSync, statSync, writeFileSync } from "fs";
|
|
1456
|
-
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";
|
|
1457
1725
|
function parseArgs(args) {
|
|
1458
1726
|
let target = "dashboard.json";
|
|
1459
1727
|
let title = "my dashboard";
|
|
1460
1728
|
let author = "";
|
|
1461
1729
|
let force = false;
|
|
1730
|
+
let makeDefault = false;
|
|
1462
1731
|
for (let i = 0; i < args.length; i++) {
|
|
1463
1732
|
const a = args[i];
|
|
1464
1733
|
if (a === "--title" || a === "-t") {
|
|
@@ -1471,6 +1740,8 @@ function parseArgs(args) {
|
|
|
1471
1740
|
author = a.slice("--author=".length);
|
|
1472
1741
|
} else if (a === "--force" || a === "-f") {
|
|
1473
1742
|
force = true;
|
|
1743
|
+
} else if (a === "--default") {
|
|
1744
|
+
makeDefault = true;
|
|
1474
1745
|
} else if (!a.startsWith("-")) {
|
|
1475
1746
|
target = a;
|
|
1476
1747
|
} else {
|
|
@@ -1478,12 +1749,14 @@ function parseArgs(args) {
|
|
|
1478
1749
|
}
|
|
1479
1750
|
}
|
|
1480
1751
|
if (!title.trim()) return { error: "--title cannot be empty" };
|
|
1481
|
-
return { target, title, author, force };
|
|
1752
|
+
return { target, title, author, force, makeDefault };
|
|
1482
1753
|
}
|
|
1483
1754
|
function resolveDest(target) {
|
|
1484
|
-
const
|
|
1485
|
-
if (
|
|
1486
|
-
|
|
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") };
|
|
1487
1760
|
}
|
|
1488
1761
|
function skeleton(title, author) {
|
|
1489
1762
|
return {
|
|
@@ -1524,12 +1797,17 @@ function init(args) {
|
|
|
1524
1797
|
if ("error" in parsed) {
|
|
1525
1798
|
console.error(`\u2717 ${parsed.error}`);
|
|
1526
1799
|
console.error(
|
|
1527
|
-
"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]"
|
|
1528
1801
|
);
|
|
1529
1802
|
return 1;
|
|
1530
1803
|
}
|
|
1531
|
-
const
|
|
1532
|
-
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)) {
|
|
1533
1811
|
if (!parsed.force) {
|
|
1534
1812
|
console.error(`\u2717 ${dest} already exists`);
|
|
1535
1813
|
console.error(
|
|
@@ -1537,7 +1815,7 @@ function init(args) {
|
|
|
1537
1815
|
);
|
|
1538
1816
|
return 1;
|
|
1539
1817
|
}
|
|
1540
|
-
if (!
|
|
1818
|
+
if (!statSync2(dest).isFile()) {
|
|
1541
1819
|
console.error(`\u2717 ${dest} exists but is not a file`);
|
|
1542
1820
|
return 1;
|
|
1543
1821
|
}
|
|
@@ -1551,15 +1829,26 @@ function init(args) {
|
|
|
1551
1829
|
return 1;
|
|
1552
1830
|
}
|
|
1553
1831
|
try {
|
|
1554
|
-
|
|
1555
|
-
|
|
1832
|
+
if (name) ensureHome();
|
|
1833
|
+
mkdirSync2(dirname(dest), { recursive: true });
|
|
1834
|
+
writeFileSync2(dest, `${JSON.stringify(spec, null, 2)}
|
|
1556
1835
|
`);
|
|
1557
1836
|
} catch (error) {
|
|
1558
1837
|
console.error(`\u2717 could not write ${dest}: ${error.message}`);
|
|
1559
1838
|
return 1;
|
|
1560
1839
|
}
|
|
1561
|
-
|
|
1840
|
+
let madeDefault = false;
|
|
1841
|
+
if (name && (parsed.makeDefault || getDefault() === null)) {
|
|
1842
|
+
setDefault(name);
|
|
1843
|
+
madeDefault = true;
|
|
1844
|
+
}
|
|
1845
|
+
const hint = name ?? dest;
|
|
1562
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
|
+
}
|
|
1563
1852
|
console.log(" next: read the catalogue, add frames, then lint + serve:");
|
|
1564
1853
|
console.log(" npx --yes zframes@latest catalogue");
|
|
1565
1854
|
console.log(` npx --yes zframes@latest lint ${hint}`);
|
|
@@ -1626,9 +1915,11 @@ function lintSpec(spec) {
|
|
|
1626
1915
|
}
|
|
1627
1916
|
|
|
1628
1917
|
// src/serve.ts
|
|
1629
|
-
import { existsSync as
|
|
1630
|
-
import {
|
|
1631
|
-
|
|
1918
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1919
|
+
import {
|
|
1920
|
+
createServer
|
|
1921
|
+
} from "http";
|
|
1922
|
+
import { join as join3, resolve as resolve3 } from "path";
|
|
1632
1923
|
import { fileURLToPath } from "url";
|
|
1633
1924
|
import sirv from "sirv";
|
|
1634
1925
|
|
|
@@ -1638,6 +1929,8 @@ import { readFile, writeFile } from "fs/promises";
|
|
|
1638
1929
|
// ../core/src/routes.ts
|
|
1639
1930
|
var DASHBOARD_READ_ROUTE = "/__zframes/dashboard.json";
|
|
1640
1931
|
var DASHBOARD_WRITE_ROUTE = "/__zframes/dashboard";
|
|
1932
|
+
var DASHBOARD_LIST_ROUTE = "/__zframes/dashboards";
|
|
1933
|
+
var DASHBOARD_SWITCH_ROUTE = "/__zframes/switch";
|
|
1641
1934
|
var DASHBOARD_PROXY_ROUTE = "/__zframes/proxy";
|
|
1642
1935
|
var ACCOUNT_PORTFOLIO_ROUTE = "/__zframes/account/portfolio";
|
|
1643
1936
|
var ACCOUNT_CREDENTIALS_ROUTE = "/__zframes/account/credentials";
|
|
@@ -1656,6 +1949,7 @@ var PROXY_ALLOW_HOSTS = /* @__PURE__ */ new Set([
|
|
|
1656
1949
|
"www.nyse.com",
|
|
1657
1950
|
"markets.newyorkfed.org",
|
|
1658
1951
|
"api.fiscaldata.treasury.gov",
|
|
1952
|
+
"home.treasury.gov",
|
|
1659
1953
|
"api.bls.gov",
|
|
1660
1954
|
"cdn.finra.org",
|
|
1661
1955
|
// News-outlet RSS feeds (CORS-blocked, so the news-feed frame reads them
|
|
@@ -1928,11 +2222,13 @@ ${recent.map(
|
|
|
1928
2222
|
// Primer: brief the runner on what zframes is and how it works, so a
|
|
1929
2223
|
// general-purpose agent answers as the embedded assistant rather than from
|
|
1930
2224
|
// cold. Kept tight — the catalogue + live digest below carry the specifics.
|
|
1931
|
-
`You are zAI, the assistant built into zframes \u2014 a keyless, AI-personalizable live market dashboard for crypto and stocks. Users assemble their own dashboard from "frames": self-contained widgets for live prices, funding, open interest, fear & greed, TVL and on-chain activity, macro rates, news, even games. They arrange frames on a grid in "customise" mode \u2014 drag, resize, add, remove, and configure each \u2014 and edits save to a dashboard.json the runtime renders from. All data comes from free public APIs (no keys, no accounts); stocks are Hyperliquid HIP-3
|
|
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.
|
|
1932
2226
|
|
|
1933
2227
|
${frameCatalogue}${grounding}
|
|
1934
2228
|
|
|
1935
|
-
${transcript}
|
|
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.
|
|
1936
2232
|
|
|
1937
2233
|
Question: ${question}`
|
|
1938
2234
|
);
|
|
@@ -1943,7 +2239,7 @@ function runAgent(runner, prompt, cwd, onDelta) {
|
|
|
1943
2239
|
tmpdir(),
|
|
1944
2240
|
`zframes-ask-${process.pid}-${++askCounter}.txt`
|
|
1945
2241
|
);
|
|
1946
|
-
return new Promise((
|
|
2242
|
+
return new Promise((resolve5) => {
|
|
1947
2243
|
let child;
|
|
1948
2244
|
try {
|
|
1949
2245
|
child = spawn(runner.bin, runner.buildArgs(prompt, outFile), {
|
|
@@ -1951,7 +2247,7 @@ function runAgent(runner, prompt, cwd, onDelta) {
|
|
|
1951
2247
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1952
2248
|
});
|
|
1953
2249
|
} catch (error) {
|
|
1954
|
-
|
|
2250
|
+
resolve5({ ok: false, error: String(error) });
|
|
1955
2251
|
return;
|
|
1956
2252
|
}
|
|
1957
2253
|
let stdout = "";
|
|
@@ -1962,7 +2258,7 @@ function runAgent(runner, prompt, cwd, onDelta) {
|
|
|
1962
2258
|
if (settled) return;
|
|
1963
2259
|
settled = true;
|
|
1964
2260
|
clearTimeout(timer);
|
|
1965
|
-
|
|
2261
|
+
resolve5(r);
|
|
1966
2262
|
};
|
|
1967
2263
|
const timer = setTimeout(() => {
|
|
1968
2264
|
child.kill("SIGKILL");
|
|
@@ -2108,9 +2404,7 @@ function handleAsk(req, res, specFile, catalogue) {
|
|
|
2108
2404
|
|
|
2109
2405
|
// ../core/src/account.ts
|
|
2110
2406
|
import { createHmac } from "crypto";
|
|
2111
|
-
import { mkdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
2112
|
-
import { homedir } from "os";
|
|
2113
|
-
import { join as join3 } from "path";
|
|
2407
|
+
import { chmod, mkdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
2114
2408
|
function json(res, status, body) {
|
|
2115
2409
|
res.statusCode = status;
|
|
2116
2410
|
res.setHeader("content-type", "application/json");
|
|
@@ -2151,19 +2445,21 @@ function readBody(req, res) {
|
|
|
2151
2445
|
});
|
|
2152
2446
|
});
|
|
2153
2447
|
}
|
|
2154
|
-
var CRED_DIR = join3(homedir(), ".zframes");
|
|
2155
|
-
var CRED_FILE = join3(CRED_DIR, "credentials.json");
|
|
2156
2448
|
async function readStore() {
|
|
2157
2449
|
try {
|
|
2158
|
-
const parsed = JSON.parse(
|
|
2450
|
+
const parsed = JSON.parse(
|
|
2451
|
+
await readFile3(credentialsFile(), "utf8")
|
|
2452
|
+
);
|
|
2159
2453
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
2160
2454
|
} catch {
|
|
2161
2455
|
return {};
|
|
2162
2456
|
}
|
|
2163
2457
|
}
|
|
2164
2458
|
async function writeStore(store) {
|
|
2165
|
-
await mkdir(
|
|
2166
|
-
await
|
|
2459
|
+
await mkdir(storeHome(), { recursive: true, mode: 448 });
|
|
2460
|
+
await chmod(storeHome(), 448).catch(() => {
|
|
2461
|
+
});
|
|
2462
|
+
await writeFile2(credentialsFile(), `${JSON.stringify(store, null, 2)}
|
|
2167
2463
|
`, {
|
|
2168
2464
|
encoding: "utf8",
|
|
2169
2465
|
mode: 384
|
|
@@ -2324,9 +2620,10 @@ async function handleAccountCredentials(req, res) {
|
|
|
2324
2620
|
|
|
2325
2621
|
// src/serve.ts
|
|
2326
2622
|
var DEFAULT_PORT = 37263;
|
|
2623
|
+
var MAX_SWITCH_BODY_BYTES = 1e5;
|
|
2327
2624
|
var FRAME_CATALOGUE = catalogueSummary(frameMetas);
|
|
2328
2625
|
function parseArgs2(args) {
|
|
2329
|
-
let file
|
|
2626
|
+
let file;
|
|
2330
2627
|
let port = DEFAULT_PORT;
|
|
2331
2628
|
let contact = process.env.ZFRAMES_CONTACT;
|
|
2332
2629
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -2350,31 +2647,97 @@ function parseArgs2(args) {
|
|
|
2350
2647
|
}
|
|
2351
2648
|
return { file, port, contact };
|
|
2352
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
|
+
}
|
|
2353
2656
|
function serve(args) {
|
|
2354
2657
|
const parsed = parseArgs2(args);
|
|
2355
2658
|
if ("error" in parsed) {
|
|
2356
2659
|
console.error(`\u2717 ${parsed.error}`);
|
|
2357
2660
|
console.error(
|
|
2358
|
-
"usage: zframes serve [dashboard.json] [--port <n>] [--contact <email>]"
|
|
2661
|
+
"usage: zframes serve [name|dashboard.json] [--port <n>] [--contact <email>]"
|
|
2359
2662
|
);
|
|
2360
2663
|
return Promise.resolve(1);
|
|
2361
2664
|
}
|
|
2362
|
-
const
|
|
2363
|
-
if (
|
|
2364
|
-
console.error(`\u2717
|
|
2365
|
-
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}`);
|
|
2366
2668
|
return Promise.resolve(1);
|
|
2367
2669
|
}
|
|
2368
|
-
|
|
2670
|
+
let current = target;
|
|
2671
|
+
const canSwitch = target.kind === "store";
|
|
2369
2672
|
const bundleDir = fileURLToPath(new URL("../runtime", import.meta.url));
|
|
2370
|
-
if (!
|
|
2673
|
+
if (!existsSync3(join3(bundleDir, "index.html"))) {
|
|
2371
2674
|
console.error(`\u2717 runtime bundle missing at ${bundleDir}`);
|
|
2372
2675
|
console.error(" run `pnpm build:cli` to build it.");
|
|
2373
2676
|
return Promise.resolve(1);
|
|
2374
2677
|
}
|
|
2375
2678
|
const serveBundle = sirv(bundleDir, { dev: true });
|
|
2376
|
-
|
|
2679
|
+
let serveSiblings = sirv(resolve3(current.file, ".."), { dev: true });
|
|
2377
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
|
+
}
|
|
2378
2741
|
return new Promise((done) => {
|
|
2379
2742
|
const server = createServer((req, res) => {
|
|
2380
2743
|
const rawPath = (req.url ?? "/").split("?")[0];
|
|
@@ -2388,7 +2751,7 @@ function serve(args) {
|
|
|
2388
2751
|
}
|
|
2389
2752
|
if (path === DASHBOARD_READ_ROUTE) {
|
|
2390
2753
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
2391
|
-
void handleSpecRead(file, res);
|
|
2754
|
+
void handleSpecRead(current.file, res);
|
|
2392
2755
|
} else {
|
|
2393
2756
|
res.statusCode = 405;
|
|
2394
2757
|
res.end();
|
|
@@ -2396,7 +2759,32 @@ function serve(args) {
|
|
|
2396
2759
|
return;
|
|
2397
2760
|
}
|
|
2398
2761
|
if (path === DASHBOARD_WRITE_ROUTE) {
|
|
2399
|
-
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);
|
|
2400
2788
|
return;
|
|
2401
2789
|
}
|
|
2402
2790
|
if (path === AGENTS_LIST_ROUTE) {
|
|
@@ -2409,7 +2797,7 @@ function serve(args) {
|
|
|
2409
2797
|
return;
|
|
2410
2798
|
}
|
|
2411
2799
|
if (path === ASK_ROUTE) {
|
|
2412
|
-
handleAsk(req, res, file, FRAME_CATALOGUE);
|
|
2800
|
+
handleAsk(req, res, current.file, FRAME_CATALOGUE);
|
|
2413
2801
|
return;
|
|
2414
2802
|
}
|
|
2415
2803
|
if (path === ACCOUNT_PORTFOLIO_ROUTE) {
|
|
@@ -2461,17 +2849,23 @@ function serve(args) {
|
|
|
2461
2849
|
});
|
|
2462
2850
|
server.listen(parsed.port, "127.0.0.1", () => {
|
|
2463
2851
|
const url = `http://localhost:${parsed.port}`;
|
|
2852
|
+
const label = target.kind === "store" ? `"${target.name}" from your store` : target.file;
|
|
2464
2853
|
console.log(`\u26A1 zframes is live at ${url}`);
|
|
2465
2854
|
console.log(
|
|
2466
|
-
` serving ${
|
|
2855
|
+
` serving ${label} \u2014 live editing on; drag, resize, then Save writes back.`
|
|
2467
2856
|
);
|
|
2857
|
+
if (canSwitch && listDashboards().length > 1) {
|
|
2858
|
+
console.log(
|
|
2859
|
+
" switch dashboards from the header dropdown in the app."
|
|
2860
|
+
);
|
|
2861
|
+
}
|
|
2468
2862
|
});
|
|
2469
2863
|
});
|
|
2470
2864
|
}
|
|
2471
2865
|
|
|
2472
2866
|
// src/snapshot.ts
|
|
2473
|
-
import { existsSync as
|
|
2474
|
-
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";
|
|
2475
2869
|
|
|
2476
2870
|
// ../core/src/cache.ts
|
|
2477
2871
|
var TtlCache = class {
|
|
@@ -2980,7 +3374,7 @@ var DAY_MS = 864e5;
|
|
|
2980
3374
|
function loadSpec(file) {
|
|
2981
3375
|
let raw;
|
|
2982
3376
|
try {
|
|
2983
|
-
raw =
|
|
3377
|
+
raw = readFileSync2(file, "utf8");
|
|
2984
3378
|
} catch {
|
|
2985
3379
|
console.error(`\u2717 cannot read ${file}`);
|
|
2986
3380
|
return null;
|
|
@@ -3035,6 +3429,24 @@ function flagValue(args, name) {
|
|
|
3035
3429
|
const index = args.indexOf(name);
|
|
3036
3430
|
return index >= 0 ? args[index + 1] : void 0;
|
|
3037
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
|
+
}
|
|
3038
3450
|
function runMeta(args) {
|
|
3039
3451
|
const pick = (flag, env) => flagValue(args, flag) ?? process.env[env] ?? null;
|
|
3040
3452
|
const rawConfig = pick("--config", "ZFRAMES_CONFIG");
|
|
@@ -3053,27 +3465,31 @@ function runMeta(args) {
|
|
|
3053
3465
|
config
|
|
3054
3466
|
};
|
|
3055
3467
|
}
|
|
3056
|
-
function
|
|
3057
|
-
|
|
3058
|
-
|
|
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;
|
|
3059
3473
|
try {
|
|
3060
|
-
const json2 = JSON.parse(
|
|
3474
|
+
const json2 = JSON.parse(readFileSync2(logPath, "utf8"));
|
|
3061
3475
|
const entries = json2.entries ?? [];
|
|
3062
3476
|
return entries.length ? entries[entries.length - 1] : null;
|
|
3063
3477
|
} catch {
|
|
3064
3478
|
return null;
|
|
3065
3479
|
}
|
|
3066
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)";
|
|
3067
3482
|
async function snapshot(args) {
|
|
3068
|
-
const
|
|
3069
|
-
if (
|
|
3070
|
-
console.error(
|
|
3071
|
-
|
|
3072
|
-
);
|
|
3483
|
+
const resolved = resolveServeTarget(positionalArg(args), process.cwd());
|
|
3484
|
+
if ("error" in resolved) {
|
|
3485
|
+
console.error(`\u2717 ${resolved.error}`);
|
|
3486
|
+
console.error(USAGE);
|
|
3073
3487
|
return 1;
|
|
3074
3488
|
}
|
|
3489
|
+
const file = resolved.file;
|
|
3075
3490
|
const logFlag = flagValue(args, "--log");
|
|
3076
3491
|
const dateFlag = flagValue(args, "--date");
|
|
3492
|
+
const logPath = logFlag ? resolve4(logFlag) : defaultLogPath(resolved);
|
|
3077
3493
|
const spec = loadSpec(file);
|
|
3078
3494
|
if (!spec) return 1;
|
|
3079
3495
|
const universe = symbolsFromSpec(spec);
|
|
@@ -3098,6 +3514,16 @@ async function snapshot(args) {
|
|
|
3098
3514
|
const out = {
|
|
3099
3515
|
date: dateFlag ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
3100
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
|
+
},
|
|
3101
3527
|
universe,
|
|
3102
3528
|
featured,
|
|
3103
3529
|
market: {
|
|
@@ -3109,7 +3535,7 @@ async function snapshot(args) {
|
|
|
3109
3535
|
global,
|
|
3110
3536
|
tvl: tvl ? tvl.slice(0, 12) : null
|
|
3111
3537
|
},
|
|
3112
|
-
priorEntry: loadPriorEntry(
|
|
3538
|
+
priorEntry: loadPriorEntry(logPath)
|
|
3113
3539
|
};
|
|
3114
3540
|
console.log(JSON.stringify(out, null, 2));
|
|
3115
3541
|
return 0;
|
|
@@ -3118,27 +3544,45 @@ async function snapshot(args) {
|
|
|
3118
3544
|
// src/index.ts
|
|
3119
3545
|
var HELP = `zframes \u2014 AI-personalizable market dashboards
|
|
3120
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
|
+
|
|
3121
3552
|
usage:
|
|
3122
|
-
zframes init [dir|file]
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
zframes
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
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
|
|
3137
3575
|
`;
|
|
3138
|
-
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;
|
|
3139
3583
|
let raw;
|
|
3140
3584
|
try {
|
|
3141
|
-
raw =
|
|
3585
|
+
raw = readFileSync3(file, "utf8");
|
|
3142
3586
|
} catch {
|
|
3143
3587
|
console.error(`\u2717 cannot read ${file}`);
|
|
3144
3588
|
return 1;
|
|
@@ -3186,6 +3630,11 @@ async function main() {
|
|
|
3186
3630
|
return lint(arg);
|
|
3187
3631
|
case "serve":
|
|
3188
3632
|
return serve(args.slice(1));
|
|
3633
|
+
case "list":
|
|
3634
|
+
case "ls":
|
|
3635
|
+
return list();
|
|
3636
|
+
case "use":
|
|
3637
|
+
return use(args.slice(1));
|
|
3189
3638
|
case "snapshot":
|
|
3190
3639
|
return snapshot(args.slice(1));
|
|
3191
3640
|
case "help":
|