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.
Files changed (110) hide show
  1. package/dist/index.js +533 -84
  2. package/package.json +5 -5
  3. package/runtime/assets/bitcoin-dominance-DVZsIJ3y.js +1 -0
  4. package/runtime/assets/breakeven-lskbHkqb.js +1 -0
  5. package/runtime/assets/breathing-BeUkCUAJ.js +1 -0
  6. package/runtime/assets/btc-blocks-fXBgGo4z.js +1 -0
  7. package/runtime/assets/btc-difficulty-BwyxgI0e.js +1 -0
  8. package/runtime/assets/btc-fees-Bg1rcXtN.js +1 -0
  9. package/runtime/assets/btc-hashrate-Xg8QNlTO.js +1 -0
  10. package/runtime/assets/btc-mempool-8Mfa77fO.js +1 -0
  11. package/runtime/assets/btc-shared-DeqDaCze.js +1 -0
  12. package/runtime/assets/calculator-CiXyJA9p.js +1 -0
  13. package/runtime/assets/chart-utils-BYIZMM9J.js +1 -0
  14. package/runtime/assets/checklist-D0dKSdmf.js +1 -0
  15. package/runtime/assets/clock-CNQTNh4L.js +1 -0
  16. package/runtime/assets/coin-movers-DulAF_oo.js +1 -0
  17. package/runtime/assets/content-shared-BZ1XzrJx.js +1 -0
  18. package/runtime/assets/countdown-BzZ2L6bw.js +1 -0
  19. package/runtime/assets/{d3-B3esOK0k.js → d3-xK4fOkBU.js} +1 -1
  20. package/runtime/assets/daily-analysis-RyI5KL4T.js +1 -0
  21. package/runtime/assets/day-meter-2bAj77Kz.js +1 -0
  22. package/runtime/assets/dex-volume-chart-DDPf1YAN.js +1 -0
  23. package/runtime/assets/dex-volume-treemap-jZCgRREu.js +1 -0
  24. package/runtime/assets/dice-wg979Skm.js +1 -0
  25. package/runtime/assets/dino-game-DlACWmE_.js +1 -0
  26. package/runtime/assets/divider-Chz1UsWJ.js +1 -0
  27. package/runtime/assets/drawdy-Ce7P47cx.js +1 -0
  28. package/runtime/assets/editor-BlGZ_tel.js +68 -0
  29. package/runtime/assets/editor-CHAAJRb1.css +1 -0
  30. package/runtime/assets/fear-greed-CENzXdIk.js +1 -0
  31. package/runtime/assets/feed-row-mVg1d5Zj.js +1 -0
  32. package/runtime/assets/filings-feed-2aIKzUUY.js +1 -0
  33. package/runtime/assets/financial-stress--ezsCpfR.js +1 -0
  34. package/runtime/assets/flappy-bird-DDe-IXt9.js +1 -0
  35. package/runtime/assets/format-CfAcS4D8.js +1 -0
  36. package/runtime/assets/fundamentals-v8f81ZNS.js +1 -0
  37. package/runtime/assets/funding-heatmap-DUoeFNGr.js +1 -0
  38. package/runtime/assets/funding-rate-chart-CjDA4nj7.js +1 -0
  39. package/runtime/assets/fx-board-CmXYQZi4.js +1 -0
  40. package/runtime/assets/game-ui-Dy_QYsPr.js +1 -0
  41. package/runtime/assets/heading-DMxdj9LK.js +1 -0
  42. package/runtime/assets/holiday-calendar-DxB-fxG5.js +1 -0
  43. package/runtime/assets/image-DLg33JpH.js +1 -0
  44. package/runtime/assets/index-C_O7gp2F.js +822 -0
  45. package/runtime/assets/index-DRTPEQtR.css +1 -0
  46. package/runtime/assets/index-XGtH5pVY.css +1 -0
  47. package/runtime/assets/index-jnySFpSv.js +1 -0
  48. package/runtime/assets/inflation-pulse-gcw-84PM.js +1 -0
  49. package/runtime/assets/journal-log-BX5Z_yte.js +1 -0
  50. package/runtime/assets/journal-open-XFQGIRkw.js +1 -0
  51. package/runtime/assets/journal-results-J1WCmyH2.js +1 -0
  52. package/runtime/assets/journal-score-CKhBSfYY.js +1 -0
  53. package/runtime/assets/journal-store-KCRoPnI_.js +1 -0
  54. package/runtime/assets/journal-ui-Cogk2Wkt.js +1 -0
  55. package/runtime/assets/labor-market-BV20Rl-4.js +1 -0
  56. package/runtime/assets/lightning-stats-h4fquhAg.js +1 -0
  57. package/runtime/assets/link-grid-a6NWj1Hu.js +1 -0
  58. package/runtime/assets/market-cap-treemap-CAi49oc_.js +1 -0
  59. package/runtime/assets/market-data-CvmpLFSo.js +1 -0
  60. package/runtime/assets/market-hours-6srwf0-r.js +1 -0
  61. package/runtime/assets/marquee-DDurk6bj.js +28 -0
  62. package/runtime/assets/metric-row-BVVhjsjK.js +1 -0
  63. package/runtime/assets/mini-line-chart-D-i1QqSe.js +1 -0
  64. package/runtime/assets/mining-pools-BPjCy4ZW.js +1 -0
  65. package/runtime/assets/mover-row-kQ9xt3Sp.js +1 -0
  66. package/runtime/assets/national-debt-DkOnmfd1.js +1 -0
  67. package/runtime/assets/news-feed-CFWKKqrH.js +1 -0
  68. package/runtime/assets/note-_cGEXAru.js +1 -0
  69. package/runtime/assets/open-interest-fxHh_T-V.js +1 -0
  70. package/runtime/assets/options-iv-CDArg1Op.js +1 -0
  71. package/runtime/assets/options-oi-strike-D_XIgUkk.js +1 -0
  72. package/runtime/assets/options-put-call-DG4RXjzD.js +1 -0
  73. package/runtime/assets/pomodoro-B-Jk-51G.js +1 -0
  74. package/runtime/assets/portfolio-allocation-BJepNj1X.js +1 -0
  75. package/runtime/assets/portfolio-common-D5L06PNC.js +1 -0
  76. package/runtime/assets/portfolio-holdings-DSkAZALG.js +1 -0
  77. package/runtime/assets/portfolio-value-BJw2csWV.js +1 -0
  78. package/runtime/assets/price-chart-khxI-9vJ.js +1 -0
  79. package/runtime/assets/price-compare-C-uEckke.js +1 -0
  80. package/runtime/assets/price-liveline-BDesEIbL.js +1 -0
  81. package/runtime/assets/price-ticker-BXSeNYf9.js +1 -0
  82. package/runtime/assets/protocol-fees-treemap-YtWbL1Kn.js +1 -0
  83. package/runtime/assets/protocol-tvl-chart-CEjUOIY5.js +1 -0
  84. package/runtime/assets/protocol-tvl-treemap-b31QlpnS.js +1 -0
  85. package/runtime/assets/quote-cehyepdm.js +1 -0
  86. package/runtime/assets/rates-board-Du4k0z1i.js +1 -0
  87. package/runtime/assets/returns-projector-DXnRFAES.js +1 -0
  88. package/runtime/assets/risk-reward-DZWyCrbv.js +1 -0
  89. package/runtime/assets/rules-card-BXJjNTqj.js +1 -0
  90. package/runtime/assets/session-progress-bA45Q3wk.js +1 -0
  91. package/runtime/assets/short-volume-C6cWknyR.js +1 -0
  92. package/runtime/assets/snake-DYTa97hl.js +1 -0
  93. package/runtime/assets/spotify-embed-BaH6UHeG.js +1 -0
  94. package/runtime/assets/stopwatch-CS6n0bY9.js +1 -0
  95. package/runtime/assets/top-movers-CQNx9EH5.js +1 -0
  96. package/runtime/assets/treasury-auctions-CGM8hAuU.js +1 -0
  97. package/runtime/assets/treemap-leaf-DLQawrx4.js +1 -0
  98. package/runtime/assets/tvl-treemap-C3SUFzdj.js +1 -0
  99. package/runtime/assets/ui-Bq3RQsdJ.js +1 -0
  100. package/runtime/assets/unicorn-scene-DcohHRPZ.js +1 -0
  101. package/runtime/assets/use-countdown-BmcF-qdL.js +1 -0
  102. package/runtime/assets/utils-DaT-yT0k.js +1 -0
  103. package/runtime/assets/video-WCnvLwQ_.js +1 -0
  104. package/runtime/assets/yield-curve-CpitTQ9L.js +1 -0
  105. package/runtime/index.html +2 -4
  106. package/runtime/assets/editor-DVTcqJo4.js +0 -68
  107. package/runtime/assets/editor-DnV0I9s-.css +0 -1
  108. package/runtime/assets/index-DEGnog6Q.css +0 -1
  109. package/runtime/assets/index-DPlG_EDx.js +0 -35727
  110. 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 readFileSync2 } from "fs";
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("gradient").describe(
70
- "Background style: 'gradient' (built-in dark glow), 'unicorn' (animated Unicorn Studio scene), or 'none'."
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
- const r = raw;
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
- return {
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
- return raw;
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: "gradient",
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 stock perps (xyz:TSLA) and crypto (BTC). The centerpiece frame.",
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. Stocks/HIP-3: "xyz:TSLA", "xyz:NVDA", "xyz:AAPL". Crypto: "BTC", "ETH".'
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, join, resolve } from "path";
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 abs = resolve(process.cwd(), target);
1485
- if (abs.toLowerCase().endsWith(".json")) return abs;
1486
- return resolve(abs, "dashboard.json");
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 dest = resolveDest(parsed.target);
1532
- if (existsSync(dest)) {
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 (!statSync(dest).isFile()) {
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
- mkdirSync(dirname(dest), { recursive: true });
1555
- writeFileSync(dest, `${JSON.stringify(spec, null, 2)}
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
- const hint = parsed.target.toLowerCase().endsWith(".json") ? parsed.target : join(parsed.target, "dashboard.json");
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 existsSync2, statSync as statSync2 } from "fs";
1630
- import { createServer } from "http";
1631
- import { join as join4, resolve as resolve2 } from "path";
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 equity perps (e.g. "xyz:TSLA") shown alongside crypto. Help the user read what's on their screen and the markets it tracks.
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}Answer the user's question in 2\u20134 sentences of plain text \u2014 no markdown headings, no preamble, no tool use, just the answer.
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((resolve4) => {
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
- resolve4({ ok: false, error: String(error) });
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
- resolve4(r);
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(await readFile3(CRED_FILE, "utf8"));
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(CRED_DIR, { recursive: true, mode: 448 });
2166
- await writeFile2(CRED_FILE, `${JSON.stringify(store, null, 2)}
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 = "dashboard.json";
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 file = resolve2(process.cwd(), parsed.file);
2363
- if (!existsSync2(file) || !statSync2(file).isFile()) {
2364
- console.error(`\u2717 no dashboard.json at ${parsed.file}`);
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
- const userDir = resolve2(file, "..");
2670
+ let current = target;
2671
+ const canSwitch = target.kind === "store";
2369
2672
  const bundleDir = fileURLToPath(new URL("../runtime", import.meta.url));
2370
- if (!existsSync2(join4(bundleDir, "index.html"))) {
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
- const serveSiblings = sirv(userDir, { dev: true });
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 ${parsed.file} \u2014 live editing on; drag, resize, then Save writes back.`
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 existsSync3, readFileSync } from "fs";
2474
- import { dirname as dirname3, resolve as resolve3 } from "path";
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 = readFileSync(file, "utf8");
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 loadPriorEntry(dashboardPath, logFlag) {
3057
- const logPath = logFlag ? resolve3(logFlag) : resolve3(dirname3(dashboardPath), "..", "public", "daily-analysis.json");
3058
- if (!existsSync3(logPath)) return null;
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(readFileSync(logPath, "utf8"));
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 file = args.find((arg) => !arg.startsWith("--"));
3069
- if (!file) {
3070
- console.error(
3071
- "usage: zframes snapshot <dashboard.json> [--log <file>] [--date YYYY-MM-DD]\n [--model <id>] [--effort <level>] [--config <json>] (or ZFRAMES_MODEL / ZFRAMES_EFFORT / ZFRAMES_CONFIG env)"
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(file, logFlag)
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] write a bare, valid dashboard.json (envelope only \u2014
3123
- version, author, grid, background, theme, empty
3124
- frames) for the agent to fill in; --title <t>,
3125
- --author <a>, --force to overwrite
3126
- zframes serve [file] serve <dashboard.json> (default: ./dashboard.json)
3127
- as a live, editable terminal at localhost:37263
3128
- (--port <n> to change); Save writes back to the file
3129
- zframes catalogue print the frame catalogue as JSON Schema
3130
- (this is what a generating agent reads)
3131
- zframes lint <file> validate a dashboard.json; exit 1 with readable
3132
- errors (the agent's self-correction feedback)
3133
- zframes snapshot <file> gather a keyless market snapshot for the symbols
3134
- on <dashboard.json> + the prior brief, as JSON on
3135
- stdout (the deterministic half of /zframes-brief)
3136
- zframes help this text
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(file) {
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 = readFileSync2(file, "utf8");
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":