@feralfile/cli 1.5.0 → 2.0.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.
@@ -26,6 +26,7 @@ exports.parseOpenSea = parseOpenSea;
26
26
  exports.parseSuperRare = parseSuperRare;
27
27
  exports.parseNeort = parseNeort;
28
28
  exports.parseVerse = parseVerse;
29
+ exports.parseRaster = parseRaster;
29
30
  exports.parseFeralFile = parseFeralFile;
30
31
  const ETH_ADDR = /^0x[a-fA-F0-9]{40}$/;
31
32
  const TEZOS_ADDR = /^(tz1|tz2|tz3)[1-9A-HJ-NP-Za-km-z]{33}$/;
@@ -101,6 +102,9 @@ function parseMarketplaceUrl(url) {
101
102
  if (host === 'verse.works' || host.endsWith('.verse.works')) {
102
103
  return parseVerse(url);
103
104
  }
105
+ if (host === 'raster.art' || host.endsWith('.raster.art')) {
106
+ return parseRaster(url);
107
+ }
104
108
  return null;
105
109
  }
106
110
  /**
@@ -382,6 +386,27 @@ function parseVerse(url) {
382
386
  '/items/ethereum/{contract}/{tokenId} or /series/{slug}.',
383
387
  };
384
388
  }
389
+ /**
390
+ * Raster URL forms (raster.art):
391
+ * raster.art/artwork/{slug} — artwork (series) detail page
392
+ *
393
+ * Unlike the other marketplaces, Raster is already the find flow's backend
394
+ * resolver — so a Raster URL resolves to an artwork by slug via
395
+ * `artworkBySlug` and (crucially) builds DP-1 items straight from Raster's
396
+ * own `media.contentUrl`, bypassing the FF indexer the way Neort does. This
397
+ * is what lets Raster-minted tokens (which the FF indexer doesn't carry)
398
+ * build a verifiable playlist without `--skip-verify`.
399
+ */
400
+ function parseRaster(url) {
401
+ const m = /^\/artwork\/([A-Za-z0-9][A-Za-z0-9_-]*)\/?$/.exec(url.pathname);
402
+ if (m) {
403
+ return { kind: 'raster-artwork', slug: m[1] };
404
+ }
405
+ return {
406
+ kind: 'unsupported',
407
+ reason: `Raster URL not recognized: ${url.pathname}. Expected /artwork/{slug}.`,
408
+ };
409
+ }
385
410
  /**
386
411
  * Feral File URL forms (source of truth: feralfile-client app-routing
387
412
  * + collection-routing modules):
@@ -21,18 +21,43 @@ function isTimeBasedMedia(mimeType, sourceUrl) {
21
21
  const candidates = [String(mimeType || '').toLowerCase(), detectMimeType(sourceUrl)];
22
22
  return candidates.some((mime) => mime.startsWith('video/') || mime.startsWith('audio/'));
23
23
  }
24
+ /**
25
+ * isInteractiveWeb reports whether an item is an interactive web page (HTML).
26
+ *
27
+ * Such sources have no intrinsic runtime AND no end-of-stream event, so they
28
+ * are timed differently from both video/audio (which end) and static images
29
+ * (which need a display duration): they play open-ended. The FF1 player
30
+ * renders these in a sandboxed iframe.
31
+ *
32
+ * Mirrors isTimeBasedMedia's two-signal approach: an explicit `text/html`
33
+ * MIME hint or a `.html`/`.htm` URL extension qualifies.
34
+ *
35
+ * @param {string} [mimeType] - MIME type hint, if any
36
+ * @param {string} [sourceUrl] - Item source URL, used as an extension fallback
37
+ * @returns {boolean} True when the source is an interactive HTML page
38
+ */
39
+ function isInteractiveWeb(mimeType, sourceUrl) {
40
+ const candidates = [String(mimeType || '').toLowerCase(), detectMimeType(sourceUrl)];
41
+ return candidates.some((mime) => mime.startsWith('text/html'));
42
+ }
24
43
  /**
25
44
  * applyItemTiming sets a DP1 item's playback timing per DP-1 §4.1.
26
45
  *
27
46
  * Invariant this function owns: an explicit numeric `duration` always wins
28
- * and is stamped as-is. When `duration` is undefined/null ("auto"), a
29
- * time-based source (video/audio) gets NO duration and `display.loop: false`,
30
- * so a conformant player MUST advance at end-of-stream — the media plays its
31
- * natural length. Static and code-based sources fall back to the configured
32
- * `defaultDuration` because they have no intrinsic runtime to play out.
47
+ * and is stamped as-is. When `duration` is undefined/null ("auto"):
48
+ * - a time-based source (video/audio) gets NO duration and
49
+ * `display.loop: false`, so a conformant player MUST advance at
50
+ * end-of-stream — the media plays its natural length;
51
+ * - an interactive web page (HTML) gets NO duration either, but for the
52
+ * opposite reason: it has no end-of-stream event, so a conformant player
53
+ * parks on it indefinitely (the generative/interactive art keeps running);
54
+ * - static and code-based sources fall back to the configured
55
+ * `defaultDuration` because they have no intrinsic runtime to play out.
33
56
  *
34
57
  * Do not re-introduce an unconditional duration here: stamping a default on
35
- * video items silently truncates or pads them (the bug this fixed).
58
+ * video items silently truncates or pads them, and stamping one on an HTML
59
+ * page cuts the artwork off and restarts it every `defaultDuration` seconds
60
+ * (the bugs this guards against).
36
61
  *
37
62
  * @param {Object} item - DP1 playlist item (mutated in place)
38
63
  * @param {Object} mediaHints - Media signals for the time-based check
@@ -50,6 +75,11 @@ function applyItemTiming(item, mediaHints = {}, duration) {
50
75
  item.display = { ...(item.display || {}), loop: false };
51
76
  return item;
52
77
  }
78
+ if (isInteractiveWeb(mediaHints.mimeType, mediaHints.sourceUrl)) {
79
+ // No duration: an open-ended interactive page should play until the
80
+ // playlist advances for another reason, not on a fixed timer.
81
+ return item;
82
+ }
53
83
  item.duration = getDefaultStaticDuration();
54
84
  return item;
55
85
  }
@@ -496,20 +526,24 @@ function validateDP1Playlist(playlist) {
496
526
  };
497
527
  }
498
528
  /**
499
- * Detect MIME type from URL or file extension
529
+ * Detect MIME type from a URL or file extension.
500
530
  *
501
- * Analyzes URL to determine appropriate MIME type for media content.
502
- * Supports images, videos, audio, and 3D models.
531
+ * Maps known media/web extensions to a MIME type. Returns '' (empty) when the
532
+ * extension is unknown or absent — the type is genuinely undetermined, and
533
+ * callers decide the fallback. (Do NOT default to 'image/png': that silently
534
+ * misclassified HTML pages and extensionless URLs as static images, stamping
535
+ * them with a display duration that cut interactive artworks off.)
503
536
  *
504
- * @param {string} url - Media URL
505
- * @returns {string} MIME type
537
+ * @param {string} url - Media or page URL
538
+ * @returns {string} MIME type, or '' when undetermined
506
539
  * @example
507
- * const mimeType = detectMimeType('https://example.com/image.png');
508
- * // Returns: 'image/png'
540
+ * detectMimeType('https://example.com/image.png'); // 'image/png'
541
+ * detectMimeType('https://example.com/art.html'); // 'text/html'
542
+ * detectMimeType('https://example.com/output/abc'); // ''
509
543
  */
510
544
  function detectMimeType(url) {
511
545
  if (!url) {
512
- return 'image/png';
546
+ return '';
513
547
  }
514
548
  const extension = url.split('.').pop()?.toLowerCase().split('?')[0];
515
549
  const mimeTypes = {
@@ -519,6 +553,8 @@ function detectMimeType(url) {
519
553
  gif: 'image/gif',
520
554
  webp: 'image/webp',
521
555
  svg: 'image/svg+xml',
556
+ html: 'text/html',
557
+ htm: 'text/html',
522
558
  mp4: 'video/mp4',
523
559
  webm: 'video/webm',
524
560
  mp3: 'audio/mpeg',
@@ -526,7 +562,7 @@ function detectMimeType(url) {
526
562
  glb: 'model/gltf-binary',
527
563
  gltf: 'model/gltf+json',
528
564
  };
529
- return mimeTypes[extension] || 'image/png';
565
+ return mimeTypes[extension] || '';
530
566
  }
531
567
  /**
532
568
  * Build a single DP1 playlist item from a URL
@@ -536,6 +572,10 @@ function detectMimeType(url) {
536
572
  * (video/audio URLs play their natural length per DP-1 §4.1)
537
573
  * @param {Object} [options] - Optional configuration
538
574
  * @param {string} [options.title] - Optional item title override
575
+ * @param {string} [options.mimeType] - Source media type hint for timing
576
+ * (DP-1 §4.1). Needed when the URL has no file extension to detect from —
577
+ * e.g. extensionless IPFS gateway URLs, where without this hint a video
578
+ * would be misclassified as a static image and stamped a fixed duration.
539
579
  * @returns {Object} DP1 playlist item
540
580
  */
541
581
  function buildUrlItem(url, duration, options = {}) {
@@ -580,7 +620,13 @@ function buildUrlItem(url, duration, options = {}) {
580
620
  margin: 0,
581
621
  },
582
622
  };
583
- return applyItemTiming(item, { sourceUrl }, duration);
623
+ // A URL handed to `play` that isn't a recognized media file is treated as an
624
+ // interactive web page: the FF1 renders unknown URLs in a sandboxed iframe,
625
+ // so an explicit caller hint aside, "unknown" means web, not static image.
626
+ // This routes such items through the open-ended (no-duration) timing path
627
+ // instead of the static-image default.
628
+ const mimeHint = options.mimeType || (detectMimeType(sourceUrl) === '' ? 'text/html' : undefined);
629
+ return applyItemTiming(item, { sourceUrl, mimeType: mimeHint }, duration);
584
630
  }
585
631
  module.exports = {
586
632
  convertTokenToDP1Item,
@@ -591,6 +637,7 @@ module.exports = {
591
637
  validateDP1Playlist,
592
638
  detectMimeType,
593
639
  isTimeBasedMedia,
640
+ isInteractiveWeb,
594
641
  applyItemTiming,
595
642
  buildUrlItem,
596
643
  };
@@ -58,6 +58,7 @@ var __importStar = (this && this.__importStar) || (function () {
58
58
  })();
59
59
  Object.defineProperty(exports, "__esModule", { value: true });
60
60
  exports.resolveTokenToArtwork = resolveTokenToArtwork;
61
+ exports.resolveSlugToArtwork = resolveSlugToArtwork;
61
62
  exports.resolveAddressToArtist = resolveAddressToArtist;
62
63
  exports.listArtworkTokens = listArtworkTokens;
63
64
  exports.listArtistArtworks = listArtistArtworks;
@@ -150,6 +151,30 @@ async function resolveTokenToArtwork(chain, contract, tokenId) {
150
151
  artists: (artwork.artists ?? []).map(toRasterArtist),
151
152
  };
152
153
  }
154
+ /**
155
+ * Resolve a `raster.art/artwork/{slug}` URL to its artwork (series) summary.
156
+ *
157
+ * One round trip via `artworkBySlug`. Returns `null` when the slug is
158
+ * unknown (Raster models not-found as a null query field, not an HTTP 404).
159
+ */
160
+ async function resolveSlugToArtwork(slug) {
161
+ const data = await rasterQuery(`query ResolveSlug($slug: String!) {
162
+ artworkBySlug(slug: $slug) {
163
+ id
164
+ title
165
+ artists { id name slug }
166
+ }
167
+ }`, { slug });
168
+ const artwork = data.artworkBySlug;
169
+ if (!artwork) {
170
+ return null;
171
+ }
172
+ return {
173
+ artworkId: artwork.id,
174
+ title: artwork.title,
175
+ artists: (artwork.artists ?? []).map(toRasterArtist),
176
+ };
177
+ }
153
178
  /**
154
179
  * Resolve a wallet address to the Raster artist who claims it.
155
180
  *
@@ -180,7 +205,7 @@ async function listArtworkTokens(artworkId, options = {}) {
180
205
  const data = await rasterQuery(`query ArtworkTokens($id: ID!, $first: Int, $after: String) {
181
206
  artwork(id: $id) {
182
207
  tokens(first: $first, after: $after) {
183
- nodes { chainId contractAddress tokenId }
208
+ nodes { chainId contractAddress tokenId media { contentUrl contentType previewType } }
184
209
  pageInfo { hasNextPage endCursor }
185
210
  }
186
211
  }
@@ -200,6 +225,10 @@ async function listArtworkTokens(artworkId, options = {}) {
200
225
  chain,
201
226
  contractAddress: raw.contractAddress,
202
227
  tokenId: raw.tokenId,
228
+ mediaUrl: raw.media?.contentUrl ?? null,
229
+ // Prefer a real MIME (`contentType`) when Raster populates it; today it
230
+ // is usually empty, so fall back to the category-prefixed `previewType`.
231
+ mediaType: raw.media?.contentType || raw.media?.previewType || null,
203
232
  });
204
233
  }
205
234
  const { hasNextPage, endCursor } = data.artwork.tokens.pageInfo;
@@ -21,35 +21,10 @@ npm run dev -- config show
21
21
 
22
22
  ## Top‑level fields
23
23
 
24
- - **defaultModel** (string)
25
- - The default AI model key to use. Must match a key under `models`.
26
- - Used by orchestration to pick API, timeouts, and model identifier.
27
-
28
24
  - **defaultDuration** (number, seconds)
29
25
  - Default per‑item display duration for media without an intrinsic runtime (images, code-based works). Falls back to 10s when unset.
30
26
  - Does NOT apply to video/audio items: with no explicit duration those are emitted without `duration` and with `display.loop: false`, so a DP-1 player advances at end-of-stream (§4.1) — the media plays its natural length.
31
27
 
32
- ## models
33
-
34
- Each key under `models` defines a model configuration used by the AI orchestrator.
35
-
36
- - `<modelName>.apiKey` (string): API key for the provider.
37
- - `<modelName>.baseURL` (string): Base API URL.
38
- - `<modelName>.model` (string): Model identifier (e.g., `grok-beta`, `gpt-4o`).
39
- - `<modelName>.availableModels` (string[], optional): Display/help only.
40
- - `<modelName>.timeout` (number, ms): HTTP timeout for requests.
41
- - `<modelName>.maxRetries` (number): Retry count for requests.
42
- - `<modelName>.temperature` (number): Generation temperature.
43
- - `<modelName>.maxTokens` (number): Token cap.
44
- - `<modelName>.supportsFunctionCalling` (boolean): Must be true; otherwise the CLI rejects the model.
45
-
46
- Environment variable helpers:
47
-
48
- - Anthropic (Claude): `ANTHROPIC_API_KEY`
49
- - Grok: `GROK_API_KEY`, `GROK_MODEL`, `GROK_API_BASE_URL`
50
- - OpenAI: `OPENAI_API_KEY`
51
- - Gemini: `GEMINI_API_KEY`
52
-
53
28
  ## browser
54
29
 
55
30
  Optional settings used where headless/browser‑like behavior is needed.
@@ -182,15 +157,6 @@ Minimal `config.json` example (selected fields):
182
157
 
183
158
  ```json
184
159
  {
185
- "defaultModel": "claude",
186
- "models": {
187
- "claude": {
188
- "apiKey": "sk-ant-your-api-key-here",
189
- "baseURL": "https://api.anthropic.com/v1/",
190
- "model": "claude-sonnet-4-6",
191
- "supportsFunctionCalling": true
192
- }
193
- },
194
160
  "defaultDuration": 10,
195
161
  "playlist": {
196
162
  "privateKey": "your_ed25519_private_key_hex_or_base64_here",