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.
- package/README.md +0 -8
- package/dist/index.js +411 -106
- package/dist/src/ai-orchestrator/index.js +21 -21
- package/dist/src/config.js +47 -11
- package/dist/src/intent-parser/index.js +107 -112
- package/dist/src/intent-parser/utils.js +2 -5
- package/dist/src/logger.js +1 -1
- package/dist/src/main.js +41 -28
- package/dist/src/utilities/domain-resolver.js +2 -2
- package/dist/src/utilities/feed-fetcher.js +2 -2
- package/dist/src/utilities/functions.js +12 -12
- package/dist/src/utilities/index.js +65 -14
- package/dist/src/utilities/nft-indexer.js +30 -7
- package/dist/src/utilities/playlist-send.js +18 -18
- package/dist/src/utilities/playlist-verifier.js +11 -11
- package/docs/EXAMPLES.md +9 -4
- package/docs/README.md +2 -2
- package/package.json +2 -2
|
@@ -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.
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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('\
|
|
83
|
+
console.log(chalk.green('\nSent to device'));
|
|
84
84
|
if (result.deviceName) {
|
|
85
|
-
console.log(chalk.
|
|
85
|
+
console.log(chalk.dim(` ${result.deviceName}`));
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
else {
|
|
89
|
-
console.error(chalk.red('\
|
|
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
|
|
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('\
|
|
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('
|
|
175
|
+
console.log(chalk.green('Playlist looks good'));
|
|
176
176
|
if (playlist.title) {
|
|
177
|
-
console.log(chalk.
|
|
177
|
+
console.log(chalk.dim(` Title: ${playlist.title}`));
|
|
178
178
|
}
|
|
179
179
|
if (playlist.items) {
|
|
180
|
-
console.log(chalk.
|
|
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('
|
|
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.
|
|
252
|
+
console.log(chalk.dim(` • ${r.address} (${typeLabel})`));
|
|
253
253
|
if (r.normalized) {
|
|
254
|
-
console.log(chalk.
|
|
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('\
|
|
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 =
|
|
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.
|
|
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.
|
|
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(`
|
|
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.
|
|
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
|
-
//
|
|
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(`
|
|
348
|
+
console.error(chalk.red(` Failed: ${error.message}`));
|
|
298
349
|
if (verbose) {
|
|
299
|
-
console.error(chalk.
|
|
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.
|
|
385
|
+
console.log(chalk.dim(` Playlist ID: ${publishResult.playlistId}`));
|
|
335
386
|
}
|
|
336
387
|
if (publishResult.feedServer) {
|
|
337
|
-
console.log(chalk.
|
|
388
|
+
console.log(chalk.dim(` Server: ${publishResult.feedServer}`));
|
|
338
389
|
}
|
|
339
390
|
}
|
|
340
391
|
else {
|
|
341
|
-
console.error(chalk.red(
|
|
392
|
+
console.error(chalk.red(`Publish failed: ${publishResult.error}`));
|
|
342
393
|
if (publishResult.message) {
|
|
343
|
-
console.error(chalk.
|
|
394
|
+
console.error(chalk.dim(` ${publishResult.message}`));
|
|
344
395
|
}
|
|
345
396
|
}
|
|
346
397
|
}
|
|
347
398
|
catch (error) {
|
|
348
|
-
console.error(chalk.red(
|
|
399
|
+
console.error(chalk.red(`Publish failed: ${error.message}`));
|
|
349
400
|
if (verbose) {
|
|
350
|
-
console.error(chalk.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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(`
|
|
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('
|
|
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(
|
|
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('
|
|
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('
|
|
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('
|
|
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.
|
|
169
|
-
console.error(chalk_1.default.
|
|
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.
|
|
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(`
|
|
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('
|
|
197
|
-
console.log(chalk_1.default.
|
|
198
|
-
console.log(chalk_1.default.
|
|
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.
|
|
200
|
+
console.log(chalk_1.default.dim(` Device: ${selectedDevice}`));
|
|
201
201
|
}
|
|
202
202
|
else if (availableDevices.length > 1) {
|
|
203
|
-
console.log(chalk_1.default.
|
|
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(
|
|
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('\
|
|
128
|
+
console.log(chalk_1.default.green('\nPlaylist is valid'));
|
|
129
129
|
if (filename) {
|
|
130
|
-
console.log(chalk_1.default.
|
|
130
|
+
console.log(chalk_1.default.dim(` File: ${filename}`));
|
|
131
131
|
}
|
|
132
132
|
if (result.playlist) {
|
|
133
|
-
console.log(chalk_1.default.
|
|
134
|
-
console.log(chalk_1.default.
|
|
135
|
-
console.log(chalk_1.default.
|
|
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.
|
|
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('\
|
|
143
|
+
console.log(chalk_1.default.red('\nPlaylist validation failed'));
|
|
144
144
|
if (filename) {
|
|
145
|
-
console.log(chalk_1.default.
|
|
145
|
+
console.log(chalk_1.default.dim(` File: ${filename}`));
|
|
146
146
|
}
|
|
147
|
-
console.log(chalk_1.default.red(`
|
|
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
|
|
149
|
+
console.log(chalk_1.default.yellow('\n Validation errors:'));
|
|
150
150
|
result.details.forEach((detail) => {
|
|
151
|
-
console.log(chalk_1.default.yellow(`
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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|
|
|
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.
|
|
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": "
|
|
7
|
+
"ff1": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|