@feralfile/cli 1.1.1 → 1.2.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 (36) hide show
  1. package/README.md +26 -0
  2. package/dist/index.js +4 -2
  3. package/dist/package.json +84 -0
  4. package/dist/src/ai-orchestrator/index.js +14 -2
  5. package/dist/src/commands/build.js +3 -3
  6. package/dist/src/commands/chat.js +3 -3
  7. package/dist/src/commands/config.js +2 -2
  8. package/dist/src/commands/device.js +6 -6
  9. package/dist/src/commands/find.js +502 -0
  10. package/dist/src/commands/helpers/device-discovery.js +1 -1
  11. package/dist/src/commands/play.js +52 -11
  12. package/dist/src/commands/setup.js +1 -1
  13. package/dist/src/commands/status.js +2 -2
  14. package/dist/src/intent-parser/index.js +16 -5
  15. package/dist/src/intent-parser/utils.js +7 -2
  16. package/dist/src/main.js +61 -17
  17. package/dist/src/utilities/ab-marketplace.js +95 -0
  18. package/dist/src/utilities/feral-file-artwork.js +89 -0
  19. package/dist/src/utilities/ff-marketplace.js +115 -0
  20. package/dist/src/utilities/ff1-device.js +4 -4
  21. package/dist/src/utilities/functions.js +1 -1
  22. package/dist/src/utilities/fxhash-marketplace.js +146 -0
  23. package/dist/src/utilities/index.js +80 -1
  24. package/dist/src/utilities/marketplace-url.js +373 -0
  25. package/dist/src/utilities/neort-marketplace.js +102 -0
  26. package/dist/src/utilities/nft-indexer.js +22 -14
  27. package/dist/src/utilities/objkt-marketplace.js +77 -0
  28. package/dist/src/utilities/playlist-publisher.js +9 -42
  29. package/dist/src/utilities/raster-client.js +252 -0
  30. package/dist/src/utilities/user-agent.js +20 -0
  31. package/docs/CONFIGURATION.md +3 -1
  32. package/docs/EXAMPLES.md +23 -0
  33. package/docs/FUNCTION_CALLING.md +3 -1
  34. package/docs/README.md +11 -10
  35. package/docs/RELEASING.md +92 -22
  36. package/package.json +9 -1
@@ -121,6 +121,9 @@ REQUIREMENT TYPES (BUILD)
121
121
  - "tokens 1, 2, 3 from contract 0x123" → build_playlist with tokenIds: ["1", "2", "3"]
122
122
  - "100 items from ethereum contract 0xABC" → build_playlist with quantity: 100, NO tokenIds
123
123
  - "50 random tokens from tezos contract KT1..." → build_playlist with quantity: 50, NO tokenIds
124
+ - feral_file_artwork: { type, artworkId } for Feral File artwork URLs or public artwork ids.
125
+ • USE THIS only for feralfile.com/exhibitions/artwork URLs or explicit Feral File public artwork ids
126
+ • Do not use this for ordinary blockchain contract prompts
124
127
  - query_address: { type, ownerAddress: 0x…|tz…|domain.eth|domain.tez, quantity?: number | "all" }
125
128
  • USE THIS for owner/wallet addresses WITHOUT the word "contract"
126
129
  • Patterns: "N items from [address]", "NFTs from [address]", "tokens owned by [address]"
@@ -153,6 +156,10 @@ EXAMPLES (build_playlist - contract addresses)
153
156
  - "100 random tokens from tezos contract KT1abc" → \`build_playlist\` { blockchain: "tezos", contractAddress: "KT1abc", quantity: 100 }
154
157
  - "get 50 from contract 0xDEF" → \`build_playlist\` { blockchain: "ethereum", contractAddress: "0xDEF", quantity: 50 }
155
158
 
159
+ EXAMPLES (feral_file_artwork - public artwork URLs)
160
+ - "build a playlist from https://feralfile.com/exhibitions/artwork/abc123" → \`feral_file_artwork\` { artworkId: "abc123" }
161
+ - "play Feral File artwork f0240e04d64717e319584957f6a83954b029254ad1260b6320472ea8c0c5b1cf" → \`feral_file_artwork\` { artworkId: "f0240e04d64717e319584957f6a83954b029254ad1260b6320472ea8c0c5b1cf" }
162
+
156
163
  EXAMPLES (fetch_feed)
157
164
  - "Pick 3 artworks from Social Codes and 2 from a2p. Mix them up." → \`fetch_feed\` { playlistName: "Social Codes", quantity: 3 } + \`fetch_feed\` { playlistName: "a2p", quantity: 2 }, and set \`playlistSettings.preserveOrder\` = false
158
165
 
@@ -266,20 +273,20 @@ const intentParserFunctionSchemas = [
266
273
  type: 'function',
267
274
  function: {
268
275
  name: 'parse_requirements',
269
- description: 'Parse validated and complete requirements. Only call this when you have all required information for each requirement: blockchain, contract address, token IDs, and source.',
276
+ description: 'Parse validated and complete requirements. Only call this when you have all required information for each requirement, such as blockchain/contract/token IDs, Feral File artwork id, feed playlist name, or owner address.',
270
277
  parameters: {
271
278
  type: 'object',
272
279
  properties: {
273
280
  requirements: {
274
281
  type: 'array',
275
- description: 'Array of parsed requirements. Each can be either build_playlist (specific NFTs), fetch_feed (feed playlist), or query_address (all NFTs from address)',
282
+ description: 'Array of parsed requirements. Each can be build_playlist (specific NFTs), feral_file_artwork (Feral File public artwork id or URL), fetch_feed (feed playlist), or query_address (all NFTs from address)',
276
283
  items: {
277
284
  type: 'object',
278
285
  properties: {
279
286
  type: {
280
287
  type: 'string',
281
- enum: ['build_playlist', 'fetch_feed', 'query_address'],
282
- description: 'Type of requirement: build_playlist (specific NFTs), fetch_feed (feed playlist), or query_address (all NFTs from address)',
288
+ enum: ['build_playlist', 'feral_file_artwork', 'fetch_feed', 'query_address'],
289
+ description: 'Type of requirement: build_playlist (specific NFTs), feral_file_artwork (Feral File artwork), fetch_feed (feed playlist), or query_address (all NFTs from address)',
283
290
  },
284
291
  blockchain: {
285
292
  type: 'string',
@@ -296,6 +303,10 @@ const intentParserFunctionSchemas = [
296
303
  type: 'string',
297
304
  },
298
305
  },
306
+ artworkId: {
307
+ type: 'string',
308
+ description: 'Feral File public artwork id or artwork URL - only for feral_file_artwork type',
309
+ },
299
310
  ownerAddress: {
300
311
  type: 'string',
301
312
  description: 'Owner wallet address (0x... for Ethereum, tz... for Tezos) - only for query_address type',
@@ -1067,7 +1078,7 @@ async function processIntentParserRequest(userRequest, options = {}) {
1067
1078
  const { confirmPlaylistForSending } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-send')));
1068
1079
  // Validate and confirm the playlist.
1069
1080
  // args.deviceName may be null/undefined/"null" when the model omits it;
1070
- // fall back to the CLI --device flag so `ff1 chat --device kitchen` works.
1081
+ // fall back to the CLI --device flag so `ff-cli chat --device kitchen` works.
1071
1082
  const resolvedDeviceName = args.deviceName && args.deviceName !== 'null' ? args.deviceName : defaultDeviceName;
1072
1083
  const confirmation = await confirmPlaylistForSending(args.filePath, resolvedDeviceName);
1073
1084
  if (!confirmation.success) {
@@ -51,6 +51,11 @@ function applyConstraints(params, config) {
51
51
  throw new Error(`Requirement ${index + 1}: playlistName is required for fetch_feed`);
52
52
  }
53
53
  }
54
+ else if (r.type === 'feral_file_artwork') {
55
+ if (!r.artworkId) {
56
+ throw new Error(`Requirement ${index + 1}: artworkId is required for feral_file_artwork`);
57
+ }
58
+ }
54
59
  else {
55
60
  throw new Error(`Requirement ${index + 1}: invalid type "${r.type}"`);
56
61
  }
@@ -79,9 +84,9 @@ function applyConstraints(params, config) {
79
84
  });
80
85
  // Note: No cap needed - registry system handles large playlists efficiently
81
86
  // Full items are stored in memory, only IDs are sent to AI model
82
- const hasAllQuantity = params.requirements.some((r) => r.quantity === 'all');
87
+ const hasAllQuantity = params.requirements.some((r) => 'quantity' in r && r.quantity === 'all');
83
88
  const totalRequested = params.requirements.reduce((sum, r) => {
84
- if (typeof r.quantity === 'number') {
89
+ if ('quantity' in r && typeof r.quantity === 'number') {
85
90
  return sum + r.quantity;
86
91
  }
87
92
  return sum;
package/dist/src/main.js CHANGED
@@ -44,6 +44,7 @@ exports.SEND_SHORTCUT_PATTERN = void 0;
44
44
  exports.resolveSendShortcutDevice = resolveSendShortcutDevice;
45
45
  exports.resolveEffectiveDeviceName = resolveEffectiveDeviceName;
46
46
  exports.resolveSendPlaylistDeviceName = resolveSendPlaylistDeviceName;
47
+ exports.sendPreparedPlaylistToDevice = sendPreparedPlaylistToDevice;
47
48
  exports.validateRequirements = validateRequirements;
48
49
  exports.applyPlaylistDefaults = applyPlaylistDefaults;
49
50
  exports.buildPlaylistDirect = buildPlaylistDirect;
@@ -106,6 +107,36 @@ function resolveSendPlaylistDeviceName(intentDeviceName, cliDeviceName) {
106
107
  : intentDeviceName;
107
108
  return resolveEffectiveDeviceName(sanitized, cliDeviceName);
108
109
  }
110
+ /**
111
+ * Verify a playlist for device delivery and send it using the supplied transport.
112
+ *
113
+ * This helper keeps the send branches in `buildPlaylist` testable without
114
+ * duplicating the verification and transport wiring in tests.
115
+ */
116
+ async function sendPreparedPlaylistToDevice(playlist, deps) {
117
+ const verificationResult = await deps.verifyPlaylist(playlist);
118
+ if (!verificationResult.valid) {
119
+ return {
120
+ success: false,
121
+ error: verificationResult.error || 'Failed to verify playlist before sending',
122
+ playlist: null,
123
+ };
124
+ }
125
+ const sendResult = await deps.sendToDevice(playlist, deps.resolveDeviceName());
126
+ if (sendResult.success) {
127
+ return {
128
+ success: true,
129
+ playlist,
130
+ deviceName: sendResult.deviceName,
131
+ device: sendResult.device,
132
+ };
133
+ }
134
+ return {
135
+ success: false,
136
+ error: sendResult.error || 'Failed to send playlist',
137
+ playlist: null,
138
+ };
139
+ }
109
140
  /**
110
141
  * Validate and apply constraints to requirements
111
142
  *
@@ -129,6 +160,12 @@ function validateRequirements(requirements) {
129
160
  quantity,
130
161
  };
131
162
  }
163
+ if (req.type === 'feral_file_artwork') {
164
+ if (!req.artworkId) {
165
+ throw new Error(`Requirement ${index + 1}: artworkId is required for feral_file_artwork`);
166
+ }
167
+ return req;
168
+ }
132
169
  // Query address requirement
133
170
  if (req.type === 'query_address') {
134
171
  // Query all NFTs from an owner address
@@ -249,6 +286,7 @@ async function buildPlaylist(userRequest, options = {}) {
249
286
  if (sendMatch) {
250
287
  const deviceName = sendMatch[1]?.trim() || defaultDeviceName;
251
288
  const { confirmPlaylistForSending } = await Promise.resolve().then(() => __importStar(require('./utilities/playlist-send')));
289
+ const { verifyPlaylist } = await Promise.resolve().then(() => __importStar(require('./utilities/playlist-verifier')));
252
290
  const confirmation = await confirmPlaylistForSending(outputPath, deviceName);
253
291
  if (!confirmation.success) {
254
292
  if (confirmation.message) {
@@ -261,7 +299,11 @@ async function buildPlaylist(userRequest, options = {}) {
261
299
  playlist: null,
262
300
  };
263
301
  }
264
- const sendResult = await utilities.sendToDevice(confirmation.playlist, confirmation.deviceName);
302
+ const sendResult = await sendPreparedPlaylistToDevice(confirmation.playlist, {
303
+ verifyPlaylist,
304
+ sendToDevice: utilities.sendToDevice,
305
+ resolveDeviceName: () => confirmation.deviceName,
306
+ });
265
307
  if (sendResult.success) {
266
308
  console.log(chalk_1.default.green('\nPlaylist sent'));
267
309
  if (sendResult.deviceName) {
@@ -270,7 +312,7 @@ async function buildPlaylist(userRequest, options = {}) {
270
312
  console.log();
271
313
  return {
272
314
  success: true,
273
- playlist: confirmation.playlist,
315
+ playlist: sendResult.playlist,
274
316
  action: 'send_playlist',
275
317
  };
276
318
  }
@@ -348,23 +390,15 @@ async function buildPlaylist(userRequest, options = {}) {
348
390
  // Handle playlist sending directly
349
391
  const sendParams = params;
350
392
  const utilities = getUtilities();
393
+ const { verifyPlaylist } = await Promise.resolve().then(() => __importStar(require('./utilities/playlist-verifier')));
351
394
  console.log();
352
395
  console.log(chalk_1.default.cyan('Sending to device'));
353
- const sendResult = await utilities.sendToDevice(sendParams.playlist, resolveSendPlaylistDeviceName(sendParams.deviceName, defaultDeviceName));
354
- if (sendResult.success) {
355
- console.log(chalk_1.default.green('\nPlaylist sent'));
356
- if (sendResult.deviceName) {
357
- console.log(chalk_1.default.dim(` Device: ${sendResult.deviceName}`));
358
- }
359
- console.log();
360
- return {
361
- success: true,
362
- playlist: sendParams.playlist,
363
- action: 'send_playlist',
364
- };
365
- }
366
- else {
367
- // Send failed - return error without showing the playlist summary
396
+ const sendResult = await sendPreparedPlaylistToDevice(sendParams.playlist, {
397
+ verifyPlaylist,
398
+ sendToDevice: utilities.sendToDevice,
399
+ resolveDeviceName: () => resolveSendPlaylistDeviceName(sendParams.deviceName, defaultDeviceName),
400
+ });
401
+ if (!sendResult.success) {
368
402
  console.log();
369
403
  console.error(chalk_1.default.red('Send failed'));
370
404
  if (sendResult.error) {
@@ -377,6 +411,16 @@ async function buildPlaylist(userRequest, options = {}) {
377
411
  action: 'send_playlist',
378
412
  };
379
413
  }
414
+ console.log(chalk_1.default.green('\nPlaylist sent'));
415
+ if (sendResult.deviceName) {
416
+ console.log(chalk_1.default.dim(` Device: ${sendResult.deviceName}`));
417
+ }
418
+ console.log();
419
+ return {
420
+ success: true,
421
+ playlist: sendResult.playlist,
422
+ action: 'send_playlist',
423
+ };
380
424
  }
381
425
  // Check if this is a publish_playlist action
382
426
  if (params && params.action === 'publish_playlist') {
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ /**
3
+ * Art Blocks marketplace resolver.
4
+ *
5
+ * AB collection URLs (`artblocks.io/collection/{slug}`) point to a series,
6
+ * not a token. Their public Hasura GraphQL at `artblocks-mainnet.hasura.app`
7
+ * resolves the slug to a contract + a representative tokenId in one call.
8
+ * From there, the standard Raster reverse-lookup enumerates the rest of the
9
+ * series like any other marketplace.
10
+ *
11
+ * AB does not publish a first-party MCP server as of 2026-05-17, so the
12
+ * direct GraphQL path is the right shape.
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.resolveArtBlocksCollection = resolveArtBlocksCollection;
49
+ const logger = __importStar(require("../logger"));
50
+ const user_agent_1 = require("./user-agent");
51
+ const AB_GRAPHQL = 'https://artblocks-mainnet.hasura.app/v1/graphql';
52
+ /**
53
+ * Resolve an Art Blocks collection slug to its contract address + the first
54
+ * minted tokenId in the series.
55
+ *
56
+ * @throws When the slug is unknown, the project has no minted tokens, or
57
+ * the GraphQL call fails.
58
+ */
59
+ async function resolveArtBlocksCollection(slug) {
60
+ logger.debug(`[AB] Resolving collection slug "${slug}"`);
61
+ const query = 'query ($slug: String!) {' +
62
+ ' projects_metadata(where: { slug: { _eq: $slug } }, limit: 1) {' +
63
+ ' contract_address' +
64
+ ' tokens(limit: 1, order_by: { invocation: asc }) { token_id }' +
65
+ ' }' +
66
+ '}';
67
+ const response = await fetch(AB_GRAPHQL, {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json', 'User-Agent': user_agent_1.USER_AGENT },
70
+ body: JSON.stringify({ query, variables: { slug } }),
71
+ });
72
+ if (!response.ok) {
73
+ throw new Error(`Art Blocks GraphQL ${response.status} ${response.statusText}`);
74
+ }
75
+ const data = (await response.json());
76
+ if (data.errors && data.errors.length > 0) {
77
+ throw new Error(`Art Blocks GraphQL errors: ${data.errors.map((e) => e.message).join('; ')}`);
78
+ }
79
+ const project = data.data?.projects_metadata?.[0];
80
+ if (!project) {
81
+ throw new Error(`Art Blocks: no project found for slug "${slug}". ` +
82
+ 'Check the URL — slugs come from artblocks.io/collection/{slug}. ' +
83
+ 'AB Engine collaborations may live on a different subgraph and are not covered here yet.');
84
+ }
85
+ const firstToken = project.tokens?.[0];
86
+ if (!firstToken) {
87
+ throw new Error(`Art Blocks: project "${slug}" has no minted tokens yet. ` +
88
+ 'Paste a specific token URL once at least one edition is minted.');
89
+ }
90
+ return {
91
+ chain: 'ethereum',
92
+ contract: project.contract_address.toLowerCase(),
93
+ tokenId: firstToken.token_id,
94
+ };
95
+ }
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * Feral File public artwork API helpers.
4
+ *
5
+ * The exhibition API is the source of truth for public artwork IDs, including
6
+ * swapped artwork IDs where the public URL id differs from the on-chain token
7
+ * id. The CLI should consume the identity fields returned by the artwork
8
+ * endpoint instead of reconstructing contract identity from exhibition internals.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.resolveFeralFileArtwork = resolveFeralFileArtwork;
12
+ exports.parseFeralFileArtworkId = parseFeralFileArtworkId;
13
+ const user_agent_1 = require("./user-agent");
14
+ /**
15
+ * resolveFeralFileArtwork resolves a public Feral File artwork id into indexer
16
+ * token coordinates using the self-describing artwork API response.
17
+ *
18
+ * @param artworkId - Public artwork id from Feral File.
19
+ * @param fetchImpl - Fetch implementation, injected by tests.
20
+ * @returns Token coordinates suitable for ff-indexer-v2 lookup.
21
+ */
22
+ async function resolveFeralFileArtwork(artworkId, fetchImpl = fetch) {
23
+ const normalizedArtworkId = artworkId.trim();
24
+ if (!normalizedArtworkId) {
25
+ throw new Error('Feral File artwork id is required');
26
+ }
27
+ const url = `https://feralfile.com/api/artworks/${encodeURIComponent(normalizedArtworkId)}`;
28
+ const response = await fetchImpl(url, {
29
+ headers: { 'User-Agent': user_agent_1.USER_AGENT, Accept: 'application/json' },
30
+ });
31
+ const body = (await response.json());
32
+ if (!response.ok) {
33
+ throw new Error(body.error?.message || `Feral File artwork lookup failed (${response.status})`);
34
+ }
35
+ const artwork = body.result;
36
+ if (!artwork) {
37
+ throw new Error('Feral File artwork lookup returned no result');
38
+ }
39
+ const chain = normalizeFeralFileChain(artwork.chain);
40
+ const contractAddress = stringField(artwork.contractAddress, 'contractAddress');
41
+ const tokenId = stringField(artwork.tokenID, 'tokenID');
42
+ return { chain, contractAddress, tokenId };
43
+ }
44
+ /**
45
+ * parseFeralFileArtworkId extracts the public artwork id from supported Feral
46
+ * File artwork URLs. Non-URL strings are treated as raw artwork ids so
47
+ * structured build inputs can pass the id directly.
48
+ *
49
+ * @param input - Feral File artwork URL or raw public artwork id.
50
+ * @returns Public artwork id.
51
+ */
52
+ function parseFeralFileArtworkId(input) {
53
+ const trimmed = input.trim();
54
+ if (!trimmed) {
55
+ throw new Error('Feral File artwork id is required');
56
+ }
57
+ let parsed;
58
+ try {
59
+ parsed = new URL(trimmed);
60
+ }
61
+ catch {
62
+ return trimmed;
63
+ }
64
+ const host = parsed.hostname.replace(/^www\./, '').toLowerCase();
65
+ if (host !== 'feralfile.com') {
66
+ throw new Error(`Unsupported Feral File artwork URL host: ${parsed.hostname}`);
67
+ }
68
+ const match = parsed.pathname.match(/^\/exhibitions\/artwork\/([^/]+)\/?$/);
69
+ if (!match) {
70
+ throw new Error('Unsupported Feral File artwork URL path');
71
+ }
72
+ return decodeURIComponent(match[1]);
73
+ }
74
+ function normalizeFeralFileChain(value) {
75
+ if (typeof value !== 'string') {
76
+ throw new Error('Feral File artwork is missing chain');
77
+ }
78
+ const normalized = value.toLowerCase();
79
+ if (normalized === 'ethereum' || normalized === 'tezos') {
80
+ return normalized;
81
+ }
82
+ throw new Error(`Unsupported Feral File artwork chain: ${value}`);
83
+ }
84
+ function stringField(value, fieldName) {
85
+ if (typeof value !== 'string' || value.trim() === '') {
86
+ throw new Error(`Feral File artwork is missing ${fieldName}`);
87
+ }
88
+ return value;
89
+ }
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.resolveFeralFileToken = resolveFeralFileToken;
37
+ /**
38
+ * Feral File marketplace resolver.
39
+ *
40
+ * Both artwork and series URLs go through the self-describing
41
+ * `/api/artworks/{id}` endpoint (one GET returning chain, contract, and the
42
+ * on-chain tokenID — including the swapped tokenID case where the public
43
+ * URL id differs from the on-chain id). Series URLs add one extra hop:
44
+ * resolve the series slug, then look up its first artwork, then hand that
45
+ * artwork id to `resolveFeralFileArtwork`. The exhibition's contract list is
46
+ * not consulted — exhibitions can host multiple series across multiple
47
+ * contracts, so picking-first would silently route a Tezos series to an
48
+ * Ethereum sibling contract on the same exhibition.
49
+ *
50
+ * Show URLs are rejected: a single exhibition spans multiple series, which
51
+ * is wider than v1 supports.
52
+ */
53
+ const logger = __importStar(require("../logger"));
54
+ const feral_file_artwork_1 = require("./feral-file-artwork");
55
+ const user_agent_1 = require("./user-agent");
56
+ const FF_API_BASE = 'https://feralfile.com/api';
57
+ async function ffFetch(path) {
58
+ const url = `${FF_API_BASE}${path}`;
59
+ logger.debug(`[FF API] GET ${url}`);
60
+ const response = await fetch(url, {
61
+ headers: { 'User-Agent': user_agent_1.USER_AGENT, Accept: 'application/json' },
62
+ });
63
+ if (!response.ok) {
64
+ const body = await response.text();
65
+ throw new Error(`Feral File API ${response.status} ${response.statusText} for ${path}: ${body.slice(0, 200)}`);
66
+ }
67
+ return (await response.json());
68
+ }
69
+ /**
70
+ * Resolve a parsed Feral File URL to on-chain token coordinates. For series
71
+ * URLs we return one representative token; the Raster reverse-lookup in the
72
+ * caller resolves to the series and enumerates the rest.
73
+ */
74
+ async function resolveFeralFileToken(parsed) {
75
+ switch (parsed.urlKind) {
76
+ case 'artwork':
77
+ return resolveFromArtworkID(parsed.identifier);
78
+ case 'series':
79
+ return resolveFromSeriesSlug(parsed.identifier);
80
+ case 'show':
81
+ throw new Error('Feral File `/exhibitions/shows/{slug}` URLs span multiple series; not supported in v1. ' +
82
+ 'Paste a specific series (`/exhibitions/series/{slug}`) or artwork URL.');
83
+ }
84
+ }
85
+ async function resolveFromArtworkID(publicArtworkId) {
86
+ // /api/artworks/{id} returns chain + contractAddress + on-chain tokenID
87
+ // directly, including the swapped-id case where URL id != on-chain id.
88
+ const { chain, contractAddress, tokenId } = await (0, feral_file_artwork_1.resolveFeralFileArtwork)(publicArtworkId);
89
+ return {
90
+ chain,
91
+ contract: chain === 'ethereum' ? contractAddress.toLowerCase() : contractAddress,
92
+ tokenId,
93
+ };
94
+ }
95
+ async function resolveFromSeriesSlug(slug) {
96
+ const series = await ffFetch(`/series?slug=${encodeURIComponent(slug)}`);
97
+ if (!series.result || series.result.length === 0) {
98
+ throw new Error(`Feral File: no series found for slug "${slug}".`);
99
+ }
100
+ // Slug collisions across exhibitions are documented in ff-exhibition#3039.
101
+ // Take the first record; the user can pass the artwork URL directly to disambiguate.
102
+ if (series.result.length > 1) {
103
+ logger.warn(`[FF API] Slug "${slug}" matched ${series.result.length} series across exhibitions; ` +
104
+ 'taking the first. Pass /exhibitions/artwork/{tokenId} to disambiguate.');
105
+ }
106
+ const picked = series.result[0];
107
+ const artworks = await ffFetch(`/artworks?seriesID=${encodeURIComponent(picked.id)}`);
108
+ if (!artworks.result || artworks.result.length === 0) {
109
+ throw new Error(`Feral File: series "${slug}" has no artworks.`);
110
+ }
111
+ // Hand the representative artwork's public id to the canonical resolver so
112
+ // we get the right (chain, contract, on-chain tokenID) for THIS series —
113
+ // not whatever picking-first off the exhibition's contract list returned.
114
+ return resolveFromArtworkID(artworks.result[0].id);
115
+ }
@@ -117,13 +117,13 @@ function isTransientDeviceNetworkError(error) {
117
117
  * @example
118
118
  * // Send to first device
119
119
  * const result = await sendPlaylistToDevice({
120
- * playlist: { version: '1.0.0', title: 'My Collection', items: [...] }
120
+ * playlist: { version: '1.1.0', title: 'My Collection', items: [...] }
121
121
  * });
122
122
  *
123
123
  * @example
124
124
  * // Send to specific device by name
125
125
  * const result = await sendPlaylistToDevice({
126
- * playlist: { version: '1.0.0', title: 'My Collection', items: [...] },
126
+ * playlist: { version: '1.1.0', title: 'My Collection', items: [...] },
127
127
  * deviceName: 'Living Room Display'
128
128
  * });
129
129
  */
@@ -204,7 +204,7 @@ async function sendPlaylistToDevice({ playlist, deviceName, }) {
204
204
  success: false,
205
205
  error: `Could not reach device "${deviceLabel}" at ${device.host}`,
206
206
  details: 'Check that the device is powered on and reachable on your network. ' +
207
- 'If the device IP changed (e.g. after a factory reset), run: ff1 setup',
207
+ 'If the device IP changed (e.g. after a factory reset), run: ff-cli setup',
208
208
  };
209
209
  }
210
210
  // Check response status
@@ -239,7 +239,7 @@ async function sendPlaylistToDevice({ playlist, deviceName, }) {
239
239
  success: false,
240
240
  error: `Could not reach device "${deviceLabel}" at ${device.host}`,
241
241
  details: 'Check that the device is powered on and reachable on your network. ' +
242
- 'If the device IP changed (e.g. after a factory reset), run: ff1 setup',
242
+ 'If the device IP changed (e.g. after a factory reset), run: ff-cli setup',
243
243
  };
244
244
  }
245
245
  return {
@@ -41,7 +41,7 @@ const ff1Device = require('./ff1-device');
41
41
  const domainResolver = require('./domain-resolver');
42
42
  const logger = require('../logger');
43
43
  /**
44
- * Build DP1 v1.0.0 compliant playlist
44
+ * Build DP1 v1.1.0 compliant playlist
45
45
  *
46
46
  * This is the actual implementation called by AI orchestrator's function calling.
47
47
  * Uses core playlist-builder utilities.