ff1-cli 1.0.0 → 1.0.1

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.
@@ -277,14 +277,14 @@ function displayResolutionResults(result) {
277
277
  const successful = result.resolutions.filter((r) => r.resolved);
278
278
  if (successful.length > 0) {
279
279
  successful.forEach((resolution) => {
280
- console.log(chalk_1.default.gray(` ${resolution.domain} → ${resolution.address}`));
280
+ console.log(chalk_1.default.dim(` ${resolution.domain} → ${resolution.address}`));
281
281
  });
282
282
  }
283
283
  // Display failures (but don't make them too prominent)
284
284
  const failed = result.resolutions.filter((r) => !r.resolved);
285
285
  if (failed.length > 0) {
286
286
  failed.forEach((resolution) => {
287
- console.log(chalk_1.default.yellow(` ${resolution.domain}: ${resolution.error || 'Could not resolve'}`));
287
+ console.log(chalk_1.default.yellow(` ${resolution.domain}: ${resolution.error || 'Could not resolve'}`));
288
288
  });
289
289
  }
290
290
  console.log();
@@ -28,7 +28,7 @@ async function fetchPlaylistsFromFeed(feedUrl, limit = 100) {
28
28
  const validLimit = Math.min(limit, 100);
29
29
  const response = await fetch(`${feedUrl}/playlists?limit=${validLimit}&sort=-created`);
30
30
  if (!response.ok) {
31
- console.log(chalk.yellow(` ⚠️ Feed ${feedUrl} returned ${response.status}`));
31
+ console.log(chalk.yellow(` Feed ${feedUrl} returned ${response.status}`));
32
32
  return [];
33
33
  }
34
34
  const data = await response.json();
@@ -40,7 +40,7 @@ async function fetchPlaylistsFromFeed(feedUrl, limit = 100) {
40
40
  }));
41
41
  }
42
42
  catch (error) {
43
- console.log(chalk.yellow(` ⚠️ Failed to fetch from ${feedUrl}: ${error.message}`));
43
+ console.log(chalk.yellow(` Failed to fetch from ${feedUrl}: ${error.message}`));
44
44
  return [];
45
45
  }
46
46
  }
@@ -80,13 +80,13 @@ async function sendPlaylistToDevice(params) {
80
80
  deviceName,
81
81
  });
82
82
  if (result.success) {
83
- console.log(chalk.green('\n✓ Sent to device'));
83
+ console.log(chalk.green('\nSent to device'));
84
84
  if (result.deviceName) {
85
- console.log(chalk.gray(` ${result.deviceName}`));
85
+ console.log(chalk.dim(` ${result.deviceName}`));
86
86
  }
87
87
  }
88
88
  else {
89
- console.error(chalk.red('\n✗ Could not send to device'));
89
+ console.error(chalk.red('\nSend failed'));
90
90
  if (result.error) {
91
91
  console.error(chalk.red(` ${result.error}`));
92
92
  }
@@ -115,7 +115,7 @@ async function resolveDomains(params) {
115
115
  const { domains, displayResults = true } = params;
116
116
  if (!domains || !Array.isArray(domains) || domains.length === 0) {
117
117
  const error = 'No domains provided for resolution';
118
- console.error(chalk.red(`\n${error}`));
118
+ console.error(chalk.red(`\n${error}`));
119
119
  return {
120
120
  success: false,
121
121
  domainMap: {},
@@ -158,7 +158,7 @@ async function verifyPlaylist(params) {
158
158
  error: 'No playlist provided for verification',
159
159
  };
160
160
  }
161
- console.log(chalk.cyan('\nValidating playlist...'));
161
+ console.log(chalk.cyan('\nValidate playlist'));
162
162
  // Dynamic import to avoid circular dependency
163
163
  const playlistVerifier = await Promise.resolve().then(() => __importStar(require('./playlist-verifier')));
164
164
  const verify = playlistVerifier.verifyPlaylist ||
@@ -172,17 +172,17 @@ async function verifyPlaylist(params) {
172
172
  }
173
173
  const result = verify(playlist);
174
174
  if (result.valid) {
175
- console.log(chalk.green('Playlist looks good'));
175
+ console.log(chalk.green('Playlist looks good'));
176
176
  if (playlist.title) {
177
- console.log(chalk.gray(` Title: ${playlist.title}`));
177
+ console.log(chalk.dim(` Title: ${playlist.title}`));
178
178
  }
179
179
  if (playlist.items) {
180
- console.log(chalk.gray(` Items: ${playlist.items.length}`));
180
+ console.log(chalk.dim(` Items: ${playlist.items.length}`));
181
181
  }
182
182
  console.log();
183
183
  }
184
184
  else {
185
- console.error(chalk.red('Playlist has issues'));
185
+ console.error(chalk.red('Playlist has issues'));
186
186
  console.error(chalk.red(` ${result.error}`));
187
187
  if (result.details && result.details.length > 0) {
188
188
  console.log(chalk.yellow('\n Missing or invalid fields:'));
@@ -249,15 +249,15 @@ async function verifyAddresses(params) {
249
249
  : r.type === 'contract'
250
250
  ? 'Tezos Contract'
251
251
  : 'Tezos User';
252
- console.log(chalk.gray(` • ${r.address} (${typeLabel})`));
252
+ console.log(chalk.dim(` • ${r.address} (${typeLabel})`));
253
253
  if (r.normalized) {
254
- console.log(chalk.gray(` Checksummed: ${r.normalized}`));
254
+ console.log(chalk.dim(` Checksummed: ${r.normalized}`));
255
255
  }
256
256
  });
257
257
  console.log();
258
258
  }
259
259
  else {
260
- console.error(chalk.red('\n✗ Address validation failed'));
260
+ console.error(chalk.red('\nAddress validation failed'));
261
261
  result.errors.forEach((err) => {
262
262
  console.error(chalk.red(` • ${err}`));
263
263
  });
@@ -34,7 +34,7 @@ function initializeUtilities(_config) {
34
34
  async function queryTokensByAddress(ownerAddress, quantity, duration = 10) {
35
35
  try {
36
36
  const shouldFetchAll = quantity === 'all' || quantity === undefined || quantity === null;
37
- const batchSize = 100; // Fetch 100 tokens per page
37
+ const batchSize = 50; // Fetch 50 tokens per page
38
38
  let allTokens = [];
39
39
  let offset = 0;
40
40
  let hasMore = true;
@@ -61,7 +61,7 @@ async function queryTokensByAddress(ownerAddress, quantity, duration = 10) {
61
61
  }
62
62
  allTokens = allTokens.concat(result.tokens);
63
63
  offset += batchSize;
64
- console.log(chalk.gray(` → Fetched ${allTokens.length} tokens so far...`));
64
+ console.log(chalk.dim(` → Fetched ${allTokens.length} tokens so far...`));
65
65
  // If we got fewer tokens than the batch size, we've reached the end
66
66
  if (result.tokens.length < batchSize) {
67
67
  hasMore = false;
@@ -94,7 +94,7 @@ async function queryTokensByAddress(ownerAddress, quantity, duration = 10) {
94
94
  if (typeof quantity === 'number' && selectedTokens.length > quantity) {
95
95
  selectedTokens = shuffleArray([...selectedTokens]).slice(0, quantity);
96
96
  }
97
- console.log(chalk.grey(`✓ Got ${selectedTokens.length} token(s)`));
97
+ console.log(chalk.dim(`Got ${selectedTokens.length} token(s)`));
98
98
  // Convert tokens to DP1 items
99
99
  const items = [];
100
100
  let skippedCount = 0;
@@ -118,7 +118,7 @@ async function queryTokensByAddress(ownerAddress, quantity, duration = 10) {
118
118
  }
119
119
  }
120
120
  if (skippedCount > 0) {
121
- console.log(chalk.yellow(` Skipped ${skippedCount} token(s) with invalid data (data URIs or URLs too long)`));
121
+ console.log(chalk.yellow(` Skipped ${skippedCount} token(s) with invalid data (data URIs or URLs too long)`));
122
122
  }
123
123
  return items;
124
124
  }
@@ -150,7 +150,7 @@ async function queryRequirement(requirement, duration = 10) {
150
150
  console.log(chalk.cyan(`\nResolving domain ${ownerAddress}...`));
151
151
  const resolution = await domainResolver.resolveDomain(ownerAddress);
152
152
  if (resolution.resolved && resolution.address) {
153
- console.log(chalk.gray(` ${resolution.domain} → ${resolution.address}`));
153
+ console.log(chalk.dim(` ${resolution.domain} → ${resolution.address}`));
154
154
  // Use resolved address instead of domain
155
155
  return await queryTokensByAddress(resolution.address, quantity, duration);
156
156
  }
@@ -179,7 +179,58 @@ async function queryRequirement(requirement, duration = 10) {
179
179
  console.log(chalk.cyan(`Querying ${blockchain}${contractAddress ? ' (' + contractAddress.substring(0, 10) + '...)' : ''}...`));
180
180
  let items = [];
181
181
  try {
182
- // Handle different blockchain types
182
+ // Check if we're querying by contract (no specific token IDs) or by specific token IDs
183
+ const isContractQuery = contractAddress && (!tokenIds || tokenIds.length === 0);
184
+ if (isContractQuery) {
185
+ // Query random tokens from the contract
186
+ console.log(chalk.cyan(` Fetching ${quantity || 100} random token(s) from contract ${contractAddress.substring(0, 10)}...`));
187
+ const limit = Math.min(quantity || 100, 100);
188
+ const result = await nftIndexer.queryTokensByContract(contractAddress, limit);
189
+ if (!result.success) {
190
+ console.log(chalk.yellow(` Could not fetch tokens from contract`));
191
+ console.log(chalk.cyan(` → Trying as owner address instead...`));
192
+ return await queryTokensByAddress(contractAddress, quantity, duration);
193
+ }
194
+ if (result.tokens.length === 0) {
195
+ console.log(chalk.yellow(` No tokens found in contract ${contractAddress}`));
196
+ console.log(chalk.cyan(` → Trying as owner address instead...`));
197
+ return await queryTokensByAddress(contractAddress, quantity, duration);
198
+ }
199
+ // Convert tokens to DP1 items (similar to queryTokensByAddress logic)
200
+ let allTokens = result.tokens;
201
+ // Apply quantity limit with random selection if needed
202
+ if (quantity && allTokens.length > quantity) {
203
+ allTokens = shuffleArray([...allTokens]).slice(0, quantity);
204
+ }
205
+ console.log(chalk.dim(` Got ${allTokens.length} token(s)`));
206
+ // Convert tokens to DP1 items
207
+ const dp1Items = [];
208
+ let skippedCount = 0;
209
+ for (const token of allTokens) {
210
+ // Detect blockchain from contract address
211
+ let chain = 'ethereum';
212
+ const contractAddr = token.contract_address || token.contractAddress || '';
213
+ if (contractAddr.startsWith('KT')) {
214
+ chain = 'tezos';
215
+ }
216
+ // Map indexer token data to standard format
217
+ const tokenData = nftIndexer.mapIndexerDataToStandardFormat(token, chain);
218
+ if (tokenData.success) {
219
+ const dp1Result = nftIndexer.convertToDP1Item(tokenData, duration);
220
+ if (dp1Result.success && dp1Result.item) {
221
+ dp1Items.push(dp1Result.item);
222
+ }
223
+ else if (!dp1Result.success) {
224
+ skippedCount++;
225
+ }
226
+ }
227
+ }
228
+ if (skippedCount > 0) {
229
+ console.log(chalk.yellow(` Skipped ${skippedCount} token(s) with invalid data (data URIs or URLs too long)`));
230
+ }
231
+ return dp1Items;
232
+ }
233
+ // Handle specific token IDs (original logic)
183
234
  if (blockchain.toLowerCase() === 'tezos') {
184
235
  // Tezos NFTs
185
236
  if (tokenIds && tokenIds.length > 0) {
@@ -294,9 +345,9 @@ async function buildPlaylistDirect(params, options = {}) {
294
345
  allItems.push(...items);
295
346
  }
296
347
  catch (error) {
297
- console.error(chalk.red(`Failed: ${error.message}`));
348
+ console.error(chalk.red(` Failed: ${error.message}`));
298
349
  if (verbose) {
299
- console.error(chalk.gray(error.stack));
350
+ console.error(chalk.dim(error.stack));
300
351
  }
301
352
  }
302
353
  }
@@ -331,23 +382,23 @@ async function buildPlaylistDirect(params, options = {}) {
331
382
  if (publishResult.success) {
332
383
  console.log(chalk.green(`✓ Published to feed server`));
333
384
  if (publishResult.playlistId) {
334
- console.log(chalk.gray(` Playlist ID: ${publishResult.playlistId}`));
385
+ console.log(chalk.dim(` Playlist ID: ${publishResult.playlistId}`));
335
386
  }
336
387
  if (publishResult.feedServer) {
337
- console.log(chalk.gray(` Server: ${publishResult.feedServer}`));
388
+ console.log(chalk.dim(` Server: ${publishResult.feedServer}`));
338
389
  }
339
390
  }
340
391
  else {
341
- console.error(chalk.red(`✗ Failed to publish: ${publishResult.error}`));
392
+ console.error(chalk.red(`Publish failed: ${publishResult.error}`));
342
393
  if (publishResult.message) {
343
- console.error(chalk.gray(` ${publishResult.message}`));
394
+ console.error(chalk.dim(` ${publishResult.message}`));
344
395
  }
345
396
  }
346
397
  }
347
398
  catch (error) {
348
- console.error(chalk.red(`✗ Failed to publish: ${error.message}`));
399
+ console.error(chalk.red(`Publish failed: ${error.message}`));
349
400
  if (verbose) {
350
- console.error(chalk.gray(error.stack));
401
+ console.error(chalk.dim(error.stack));
351
402
  }
352
403
  }
353
404
  }
@@ -98,14 +98,17 @@ function buildTokenCID(chain, contractAddress, tokenId) {
98
98
  * const tokens = await queryTokens({ token_cids: ['eip155:1:erc721:0xabc:123'], owners: ['0x1234...'] });
99
99
  */
100
100
  async function queryTokens(params = {}) {
101
- const { token_cids = [], owners = [], limit = 50, offset = 0 } = params;
101
+ const { token_cids = [], owners = [], contract_addresses = [], limit = 50, offset = 0 } = params;
102
102
  // Build GraphQL query without variables - inline parameters
103
103
  // (API expects inline parameters, not variables)
104
104
  const ownerFilter = owners.length > 0 ? `owners: ${JSON.stringify(owners)},` : '';
105
105
  const tokenCidsFilter = token_cids.length > 0 ? `token_cids: ${JSON.stringify(token_cids)},` : '';
106
+ const contractFilter = contract_addresses.length > 0
107
+ ? `contract_addresses: ${JSON.stringify(contract_addresses)},`
108
+ : '';
106
109
  const query = `
107
110
  query {
108
- tokens(${ownerFilter} ${tokenCidsFilter} expands: ["enrichment_source", "metadata_media_asset", "enrichment_source_media_asset"], limit: ${limit}, offset: ${offset}) {
111
+ tokens(${ownerFilter} ${tokenCidsFilter} ${contractFilter} expands: ["enrichment_source", "metadata_media_asset", "enrichment_source_media_asset"], limit: ${limit}, offset: ${offset}) {
109
112
  items {
110
113
  token_cid
111
114
  chain
@@ -958,26 +961,45 @@ async function searchNFTs(params) {
958
961
  */
959
962
  async function queryTokensByOwner(ownerAddress, limit = 100, offset = 0) {
960
963
  try {
961
- logger.info(`[NFT Indexer] Querying tokens for owner: ${ownerAddress} (limit: ${limit}, offset: ${offset})`);
964
+ logger.info(`[NFT Indexer] Querying tokens by owner: ${ownerAddress}`);
962
965
  const tokens = await queryTokens({
963
966
  owners: [ownerAddress],
964
967
  limit,
965
968
  offset,
966
969
  });
967
- logger.info(`[NFT Indexer] Found ${tokens.length} token(s) for owner ${ownerAddress}`);
968
970
  return {
969
971
  success: true,
970
972
  tokens,
971
- count: tokens.length,
972
973
  };
973
974
  }
974
975
  catch (error) {
975
- logger.error('[NFT Indexer] Failed to query tokens by owner:', error.message);
976
+ logger.error(`[NFT Indexer] Failed to query tokens by owner: ${error.message}`);
976
977
  return {
977
978
  success: false,
979
+ tokens: [],
978
980
  error: error.message,
981
+ };
982
+ }
983
+ }
984
+ async function queryTokensByContract(contractAddress, limit = 100, offset = 0) {
985
+ try {
986
+ logger.info(`[NFT Indexer] Querying tokens by contract: ${contractAddress}`);
987
+ const tokens = await queryTokens({
988
+ contract_addresses: [contractAddress],
989
+ limit,
990
+ offset,
991
+ });
992
+ return {
993
+ success: true,
994
+ tokens,
995
+ };
996
+ }
997
+ catch (error) {
998
+ logger.error(`[NFT Indexer] Failed to query tokens by contract: ${error.message}`);
999
+ return {
1000
+ success: false,
979
1001
  tokens: [],
980
- count: 0,
1002
+ error: error.message,
981
1003
  };
982
1004
  }
983
1005
  }
@@ -998,6 +1020,7 @@ module.exports = {
998
1020
  convertToDP1Item,
999
1021
  // Address-based functions
1000
1022
  queryTokensByOwner,
1023
+ queryTokensByContract,
1001
1024
  // Unified GraphQL query
1002
1025
  queryTokens,
1003
1026
  // Workflow and polling functions
@@ -65,7 +65,7 @@ async function getAvailableDevices() {
65
65
  }
66
66
  catch (error) {
67
67
  if (process.env.DEBUG) {
68
- console.log(chalk_1.default.gray(`[DEBUG] Error loading devices: ${error.message}`));
68
+ console.log(chalk_1.default.dim(`[DEBUG] Error loading devices: ${error.message}`));
69
69
  }
70
70
  // Silently fail if config can't be loaded
71
71
  }
@@ -87,23 +87,23 @@ async function confirmPlaylistForSending(filePath, deviceName) {
87
87
  // Convert string "null" to undefined (in case model passes it literally)
88
88
  const actualDeviceName = deviceName === 'null' || deviceName === '' ? undefined : deviceName;
89
89
  if (process.env.DEBUG) {
90
- console.error(chalk_1.default.gray(`[DEBUG] confirmPlaylistForSending called with: filePath="${filePath}", deviceName="${deviceName}" -> "${actualDeviceName}"`));
90
+ console.error(chalk_1.default.dim(`[DEBUG] confirmPlaylistForSending called with: filePath="${filePath}", deviceName="${deviceName}" -> "${actualDeviceName}"`));
91
91
  }
92
92
  try {
93
93
  // Check if file exists
94
- console.log(chalk_1.default.cyan(`Checking playlist file: ${resolvedPath}...`));
94
+ console.log(chalk_1.default.cyan(`Playlist file: ${resolvedPath}`));
95
95
  let _fileExists = false;
96
96
  let playlist;
97
97
  try {
98
98
  const content = await fs_1.promises.readFile(resolvedPath, 'utf-8');
99
99
  playlist = JSON.parse(content);
100
100
  _fileExists = true;
101
- console.log(chalk_1.default.green('File found'));
101
+ console.log(chalk_1.default.green('File loaded'));
102
102
  }
103
103
  catch (error) {
104
104
  const errorMsg = error.message;
105
105
  if (errorMsg.includes('ENOENT') || errorMsg.includes('no such file')) {
106
- console.log(chalk_1.default.red(`✗ File not found: ${resolvedPath}`));
106
+ console.log(chalk_1.default.red(`File not found: ${resolvedPath}`));
107
107
  return {
108
108
  success: false,
109
109
  filePath: resolvedPath,
@@ -125,12 +125,12 @@ async function confirmPlaylistForSending(filePath, deviceName) {
125
125
  };
126
126
  }
127
127
  // Validate playlist structure
128
- console.log(chalk_1.default.cyan('Validating playlist...'));
128
+ console.log(chalk_1.default.cyan('Validation'));
129
129
  // Dynamic import to avoid circular dependency
130
130
  const { verifyPlaylist } = await Promise.resolve().then(() => __importStar(require('./playlist-verifier')));
131
131
  const verifyResult = verifyPlaylist(playlist);
132
132
  if (!verifyResult.valid) {
133
- console.log(chalk_1.default.red('Playlist validation failed'));
133
+ console.log(chalk_1.default.red('Playlist validation failed'));
134
134
  const detailLines = verifyResult.details?.map((d) => ` • ${d.path}: ${d.message}`).join('\n') ||
135
135
  verifyResult.error;
136
136
  const detailPaths = verifyResult.details?.map((d) => d.path) || [];
@@ -153,7 +153,7 @@ async function confirmPlaylistForSending(filePath, deviceName) {
153
153
  message: `This playlist doesn't match DP-1 specification.\n\nErrors:\n${detailLines}${hintText}`,
154
154
  };
155
155
  }
156
- console.log(chalk_1.default.green(' Playlist is valid'));
156
+ console.log(chalk_1.default.green('Valid DP-1 playlist'));
157
157
  // Display confirmation details
158
158
  const itemCount = playlist.items?.length || 0;
159
159
  const title = playlist.title || 'Untitled';
@@ -165,10 +165,10 @@ async function confirmPlaylistForSending(filePath, deviceName) {
165
165
  // Get available devices
166
166
  availableDevices = await getAvailableDevices();
167
167
  if (process.env.DEBUG) {
168
- console.error(chalk_1.default.gray(`[DEBUG] selectedDevice is null/undefined`));
169
- console.error(chalk_1.default.gray(`[DEBUG] Available devices found: ${availableDevices.length}`));
168
+ console.error(chalk_1.default.dim(`[DEBUG] selectedDevice is null/undefined`));
169
+ console.error(chalk_1.default.dim(`[DEBUG] Available devices found: ${availableDevices.length}`));
170
170
  availableDevices.forEach((d) => {
171
- console.error(chalk_1.default.gray(`[DEBUG] Device: ${d.name} (${d.host})`));
171
+ console.error(chalk_1.default.dim(`[DEBUG] Device: ${d.name} (${d.host})`));
172
172
  });
173
173
  }
174
174
  if (availableDevices.length === 0) {
@@ -185,7 +185,7 @@ async function confirmPlaylistForSending(filePath, deviceName) {
185
185
  else if (availableDevices.length === 1) {
186
186
  // Auto-select single device
187
187
  selectedDevice = availableDevices[0].name || availableDevices[0].host;
188
- console.log(chalk_1.default.cyan(`Auto-selecting device: ${selectedDevice}`));
188
+ console.log(chalk_1.default.cyan(`Device: ${selectedDevice} (auto)`));
189
189
  }
190
190
  else {
191
191
  // Multiple devices - need user to choose
@@ -193,14 +193,14 @@ async function confirmPlaylistForSending(filePath, deviceName) {
193
193
  }
194
194
  }
195
195
  console.log();
196
- console.log(chalk_1.default.bold('Playlist Summary:'));
197
- console.log(chalk_1.default.gray(` Title: ${title}`));
198
- console.log(chalk_1.default.gray(` Items: ${itemCount}`));
196
+ console.log(chalk_1.default.bold('Send Summary'));
197
+ console.log(chalk_1.default.dim(` Title: ${title}`));
198
+ console.log(chalk_1.default.dim(` Items: ${itemCount}`));
199
199
  if (selectedDevice) {
200
- console.log(chalk_1.default.gray(` Device: ${selectedDevice}`));
200
+ console.log(chalk_1.default.dim(` Device: ${selectedDevice}`));
201
201
  }
202
202
  else if (availableDevices.length > 1) {
203
- console.log(chalk_1.default.gray(` Device: (to be selected)`));
203
+ console.log(chalk_1.default.dim(' Device: select one'));
204
204
  }
205
205
  console.log();
206
206
  // If multiple devices, return needsDeviceSelection flag
@@ -229,7 +229,7 @@ async function confirmPlaylistForSending(filePath, deviceName) {
229
229
  }
230
230
  catch (error) {
231
231
  const errorMsg = error.message;
232
- console.log(chalk_1.default.red(`✗ Error: ${errorMsg}`));
232
+ console.log(chalk_1.default.red(`Error: ${errorMsg}`));
233
233
  return {
234
234
  success: false,
235
235
  filePath: resolvedPath,
@@ -125,30 +125,30 @@ async function verifyPlaylistFile(playlistPath) {
125
125
  */
126
126
  function printVerificationResult(result, filename) {
127
127
  if (result.valid) {
128
- console.log(chalk_1.default.green('\n✅ Playlist is valid!'));
128
+ console.log(chalk_1.default.green('\nPlaylist is valid'));
129
129
  if (filename) {
130
- console.log(chalk_1.default.gray(` File: ${filename}`));
130
+ console.log(chalk_1.default.dim(` File: ${filename}`));
131
131
  }
132
132
  if (result.playlist) {
133
- console.log(chalk_1.default.gray(` Title: ${result.playlist.title}`));
134
- console.log(chalk_1.default.gray(` Items: ${result.playlist.items?.length || 0}`));
135
- console.log(chalk_1.default.gray(` DP Version: ${result.playlist.dpVersion}`));
133
+ console.log(chalk_1.default.dim(` Title: ${result.playlist.title}`));
134
+ console.log(chalk_1.default.dim(` Items: ${result.playlist.items?.length || 0}`));
135
+ console.log(chalk_1.default.dim(` DP Version: ${result.playlist.dpVersion}`));
136
136
  if (result.playlist.signature && typeof result.playlist.signature === 'string') {
137
- console.log(chalk_1.default.gray(` Signature: ${result.playlist.signature.substring(0, 30)}...`));
137
+ console.log(chalk_1.default.dim(` Signature: ${result.playlist.signature.substring(0, 30)}...`));
138
138
  }
139
139
  }
140
140
  console.log();
141
141
  }
142
142
  else {
143
- console.log(chalk_1.default.red('\n❌ Playlist validation failed!'));
143
+ console.log(chalk_1.default.red('\nPlaylist validation failed'));
144
144
  if (filename) {
145
- console.log(chalk_1.default.gray(` File: ${filename}`));
145
+ console.log(chalk_1.default.dim(` File: ${filename}`));
146
146
  }
147
- console.log(chalk_1.default.red(` Error: ${result.error}`));
147
+ console.log(chalk_1.default.red(` Error: ${result.error}`));
148
148
  if (result.details && result.details.length > 0) {
149
- console.log(chalk_1.default.yellow('\n Validation errors:'));
149
+ console.log(chalk_1.default.yellow('\n Validation errors:'));
150
150
  result.details.forEach((detail) => {
151
- console.log(chalk_1.default.yellow(` • ${detail.path}: ${detail.message}`));
151
+ console.log(chalk_1.default.yellow(` • ${detail.path}: ${detail.message}`));
152
152
  });
153
153
  }
154
154
  console.log();
package/docs/EXAMPLES.md CHANGED
@@ -23,7 +23,7 @@ npm run dev -- chat "Get 3 items from Social Codes and 2 from 0xdef" -v
23
23
 
24
24
  # Switch model
25
25
  npm run dev -- chat "your request" --model grok
26
- npm run dev -- chat "your request" --model chatgpt
26
+ npm run dev -- chat "your request" --model gpt
27
27
  npm run dev -- chat "your request" --model gemini
28
28
  ```
29
29
 
@@ -44,7 +44,7 @@ cat examples/params-example.json | npm run dev -- build -o playlist.json
44
44
  npm run dev -- chat "Build a playlist of my Tezos works from address tz1... plus 3 from Social Codes" -v -o playlist.json
45
45
 
46
46
  # Switch model if desired
47
- npm run dev -- chat "Build playlist from Ethereum address 0x... and 2 from Social Codes" --model chatgpt -v
47
+ npm run dev -- chat "Build playlist from Ethereum address 0x... and 2 from Social Codes" --model gpt -v
48
48
  ```
49
49
 
50
50
  ### One‑shot complex prompt
@@ -92,18 +92,21 @@ npm run dev -- chat "Get 5 from Social Codes, shuffle, display on 'Living Room',
92
92
  ### How It Works
93
93
 
94
94
  **Mode 1: Build and Publish** (when sources are mentioned)
95
+
95
96
  1. Intent parser detects "publish" keywords with sources/requirements
96
97
  2. Calls `get_feed_servers` to retrieve configured servers
97
98
  3. If 1 server → uses it automatically; if 2+ servers → asks user to pick
98
99
  4. Builds playlist → verifies → publishes automatically
99
100
 
100
101
  **Mode 2: Publish Existing File** (e.g., "publish playlist")
102
+
101
103
  1. Intent parser detects "publish playlist" or similar phrases
102
104
  2. Calls `get_feed_servers` to retrieve configured servers
103
105
  3. If 1 server → uses it automatically; if 2+ servers → asks user to pick
104
106
  4. Publishes the playlist from `./playlist.json` (or specified path)
105
107
 
106
108
  Output shows:
109
+
107
110
  - Playlist build progress (Mode 1 only)
108
111
  - Device sending (if requested): `✓ Sent to device: Living Room`
109
112
  - Publishing status: `✓ Published to feed server`
@@ -187,18 +190,21 @@ Select server (0-based index): 0
187
190
  ### Error Handling
188
191
 
189
192
  **Validation failed:**
193
+
190
194
  ```
191
195
  ❌ Failed to publish playlist
192
196
  Playlist validation failed: dpVersion: Required; id: Required
193
197
  ```
194
198
 
195
199
  **File not found:**
200
+
196
201
  ```
197
202
  ❌ Failed to publish playlist
198
203
  Playlist file not found: /path/to/playlist.json
199
204
  ```
200
205
 
201
206
  **API error:**
207
+
202
208
  ```
203
209
  ❌ Failed to publish playlist
204
210
  Failed to publish: {"error":"unauthorized","message":"Invalid API key"}
@@ -233,7 +239,6 @@ npm run dev -- config show
233
239
  npm run dev -- config init
234
240
  ```
235
241
 
236
-
237
242
  ### Natural‑language one‑shot examples (proven)
238
243
 
239
244
  - **ETH contract + token IDs (shuffle/mix, generic device)**
@@ -328,4 +333,4 @@ npm run dev -- config init
328
333
  ```bash
329
334
  npm run dev -- chat "Compose a playlist from reas.eth (3 items); send to device" -o playlist-generic-device.json -v
330
335
  npm run dev -- chat "Compose a playlist from reas.eth (3 items); send to 'Living Room'" -o playlist-named-device.json -v
331
- ```
336
+ ```
package/docs/README.md CHANGED
@@ -40,7 +40,7 @@ See the full configuration reference here: `./CONFIGURATION.md`.
40
40
  "model": "grok-beta",
41
41
  "supportsFunctionCalling": true
42
42
  },
43
- "chatgpt": {
43
+ "gpt": {
44
44
  "apiKey": "sk-your-openai-key-here",
45
45
  "baseURL": "https://api.openai.com/v1",
46
46
  "model": "gpt-4o",
@@ -163,7 +163,7 @@ How it works (at a glance):
163
163
  - If `deviceName` is present, the CLI will send the validated playlist to that FF1 device.
164
164
  - If `feedServer` is present (via "publish to my feed"), the CLI will publish the playlist to the selected feed server.
165
165
 
166
- Use `--model grok|chatgpt|gemini` to switch models, or set `defaultModel` in `config.json`.
166
+ Use `--model grok|gpt|gemini` to switch models, or set `defaultModel` in `config.json`.
167
167
 
168
168
  ### Natural language publishing
169
169
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "ff1-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "CLI to fetch NFT information and build DP1 playlists using Grok API",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
- "ff1": "./dist/index.js"
7
+ "ff1": "dist/index.js"
8
8
  },
9
9
  "files": [
10
10
  "dist",