@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.
- package/README.md +6 -11
- package/config.json.example +0 -65
- package/dist/index.js +1 -3
- package/dist/package.json +3 -4
- package/dist/src/commands/config.js +15 -13
- package/dist/src/commands/find.js +55 -4
- package/dist/src/commands/play.js +55 -0
- package/dist/src/commands/setup.js +1 -73
- package/dist/src/commands/status.js +0 -13
- package/dist/src/config.js +3 -126
- package/dist/src/main.js +2 -361
- package/dist/src/utilities/ff1-device.js +55 -0
- package/dist/src/utilities/marketplace-url.js +25 -0
- package/dist/src/utilities/playlist-builder.js +63 -16
- package/dist/src/utilities/raster-client.js +30 -1
- package/docs/CONFIGURATION.md +0 -34
- package/docs/EXAMPLES.md +206 -215
- package/docs/PROJECT_SPEC.md +20 -18
- package/docs/README.md +51 -104
- package/package.json +3 -4
- package/dist/src/ai-orchestrator/index.js +0 -1040
- package/dist/src/ai-orchestrator/registry.js +0 -96
- package/dist/src/commands/chat.js +0 -189
- package/dist/src/intent-parser/index.js +0 -1393
- package/dist/src/intent-parser/utils.js +0 -116
- package/docs/FUNCTION_CALLING.md +0 -100
|
@@ -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")
|
|
29
|
-
* time-based source (video/audio) gets NO duration and
|
|
30
|
-
* so a conformant player MUST advance at
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
|
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
|
-
*
|
|
502
|
-
*
|
|
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
|
-
*
|
|
508
|
-
* //
|
|
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 '
|
|
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] || '
|
|
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
|
-
|
|
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;
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -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",
|