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.
@@ -113,38 +113,48 @@ OUTPUT CONTRACT
113
113
  - Use correct types; never truncate addresses/tokenIds; tokenIds are strings; quantity is a number.
114
114
 
115
115
  REQUIREMENT TYPES (BUILD)
116
- - build_playlist: { type, blockchain: "ethereum"|"tezos", contractAddress, tokenIds: string[], quantity?: number, source?: string }
117
- ONLY use when user explicitly provides BOTH contract address AND specific token IDs
118
- Example: "tokens 1, 2, 3 from contract 0x123"
116
+ - build_playlist: { type, blockchain: "ethereum"|"tezos", contractAddress, tokenIds?: string[], quantity?: number, source?: string }
117
+ USE THIS when user mentions "contract" with a quantity: "N items from [blockchain] contract [address]"
118
+ tokenIds is OPTIONAL - omit it when user wants random tokens from a contract
119
+ • Examples:
120
+ - "tokens 1, 2, 3 from contract 0x123" → build_playlist with tokenIds: ["1", "2", "3"]
121
+ - "100 items from ethereum contract 0xABC" → build_playlist with quantity: 100, NO tokenIds
122
+ - "50 random tokens from tezos contract KT1..." → build_playlist with quantity: 50, NO tokenIds
119
123
  - query_address: { type, ownerAddress: 0x…|tz…|domain.eth|domain.tez, quantity?: number | "all" }
120
- Domains (.eth/.tez) are OWNER DOMAINS. Do not ask for tokenIds. Do not treat as contracts.
121
- A raw 0x…/tz… without tokenIds is an OWNER ADDRESS (query_address), not a contract.
122
- CRITICAL: Phrases like "N items from [address]", "NFTs from [address]", "tokens from [address]" → query_address
123
- • Example: "30 items from 0xABC" → query_address with quantity=30
124
+ USE THIS for owner/wallet addresses WITHOUT the word "contract"
125
+ Patterns: "N items from [address]", "NFTs from [address]", "tokens owned by [address]"
126
+ Domains (.eth/.tez) are ALWAYS owner addresses
127
+ • Example: "100 items from 0xABC" (without mentioning "contract") → query_address
124
128
  • When user says "all", "all tokens", "all NFTs" → use quantity="all" (string, not number)
125
- • quantity="all" will fetch ALL tokens using pagination, can handle thousands of tokens
126
129
  - fetch_feed: { type, playlistName: string, quantity?: number (default 5) }
127
130
 
131
+ CRITICAL DISTINCTION:
132
+ - User says "contract" + address → build_playlist (queries tokens FROM that contract)
133
+ - User says just address (no "contract" word) → query_address (queries tokens OWNED by that address)
134
+ - User says ".eth" or ".tez" domain → ALWAYS query_address (owner domain)
135
+
128
136
  DOMAIN OWNER RULES (CRITICAL)
129
137
  - Interpret \`*.eth\` as an Ethereum OWNER DOMAIN → produce \`query_address\` with \`ownerAddress\` set to the domain string (e.g., \`reas.eth\`).
130
138
  - Interpret \`*.tez\` as a Tezos OWNER DOMAIN → produce \`query_address\` with \`ownerAddress\` set to the domain string (e.g., \`einstein-rosen.tez\`).
131
139
  - Never treat \.eth or \.tez as a contract or collection identifier.
132
140
  - Never invent or request \`tokenIds\` for \.eth/\.tez domains. Use \`quantity\` only.
133
141
 
134
- EXAMPLES (query_address - NO tokenIds needed)
142
+ EXAMPLES (query_address - owner/wallet addresses)
135
143
  - "Pick 3 artworks from reas.eth" → \`query_address\` { ownerAddress: "reas.eth", quantity: 3 }
136
- - "3 from einstein-rosen.tez and play on my FF1" → \`query_address\` { ownerAddress: "einstein-rosen.tez", quantity: 3 } and set \`playlistSettings.deviceName\` accordingly
144
+ - "3 from einstein-rosen.tez and play on my FF1" → \`query_address\` { ownerAddress: "einstein-rosen.tez", quantity: 3 } and set \`playlistSettings.deviceName\`
137
145
  - "create a playlist of 30 items from 0xABC" → \`query_address\` { ownerAddress: "0xABC", quantity: 30 }
138
146
  - "get 20 NFTs from 0x123" → \`query_address\` { ownerAddress: "0x123", quantity: 20 }
139
- - "create a playlist from all tokens in reas.eth" → \`query_address\` { ownerAddress: "reas.eth", quantity: "all" }
140
147
  - "get all NFTs from 0xABC" → \`query_address\` { ownerAddress: "0xABC", quantity: "all" }
141
148
 
149
+ EXAMPLES (build_playlist - contract addresses)
150
+ - "tokens 5, 10, 15 from contract 0xABC on ethereum" → \`build_playlist\` { blockchain: "ethereum", contractAddress: "0xABC", tokenIds: ["5", "10", "15"] }
151
+ - "create a playlist of 100 items from ethereum contract 0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270" → \`build_playlist\` { blockchain: "ethereum", contractAddress: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", quantity: 100 }
152
+ - "100 random tokens from tezos contract KT1abc" → \`build_playlist\` { blockchain: "tezos", contractAddress: "KT1abc", quantity: 100 }
153
+ - "get 50 from contract 0xDEF" → \`build_playlist\` { blockchain: "ethereum", contractAddress: "0xDEF", quantity: 50 }
154
+
142
155
  EXAMPLES (fetch_feed)
143
156
  - "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
144
157
 
145
- EXAMPLES (build_playlist - requires BOTH contract AND tokenIds)
146
- - "tokens 5, 10, 15 from contract 0xABC on ethereum" → \`build_playlist\` { blockchain: "ethereum", contractAddress: "0xABC", tokenIds: ["5", "10", "15"] }
147
-
148
158
  PLAYLIST SETTINGS EXTRACTION
149
159
  - durationPerItem: parse phrases (e.g., "6 seconds each" → 6)
150
160
  - preserveOrder: default true; synonyms ("shuffle", "randomize", "mix", "mix them up", "scramble") → false
@@ -167,11 +177,12 @@ MISSING INFO POLICY (ASK AT MOST ONE QUESTION)
167
177
  - send: ask for device name only if user specifies a device by name and it's ambiguous; for generic references, always use get_configured_devices
168
178
 
169
179
  ADDRESS VALIDATION (CRITICAL)
170
- - When user enters any Ethereum (0x...) or Tezos (tz.../KT1) addresses, IMMEDIATELY call verify_addresses() BEFORE parsing requirements
180
+ - When user enters any Ethereum (0x...) or Tezos (tz.../KT1/KT...) addresses, IMMEDIATELY call verify_addresses() BEFORE parsing requirements
171
181
  - This includes: contract addresses in build_playlist, owner addresses in query_address, or any wallet/contract address mentioned
172
182
  - Example: user says "get tokens from 0xABC" → first call verify_addresses(['0xABC']) → get validation result → then parse_requirements
173
183
  - If verify_addresses returns valid=false, show user the error and ask them to provide the correct address
174
- - If valid=true, proceed to parse_requirements with the verified addresses
184
+ - If valid=true, the validation result shows the blockchain type (ethereum or tezos) - use this information when parsing requirements
185
+ - IMPORTANT: When user explicitly mentions blockchain (e.g., "ethereum contract" or "tezos contract"), you already know the blockchain type - DO NOT ask for it again
175
186
 
176
187
  FREE‑FORM COLLECTION NAMES
177
188
  - Treat as fetch_feed; do not guess contracts. If user says "some", default quantity = 5.
@@ -279,7 +290,7 @@ const intentParserFunctionSchemas = [
279
290
  },
280
291
  tokenIds: {
281
292
  type: 'array',
282
- description: 'Array of token IDs to fetch - only for build_playlist type',
293
+ description: 'Array of specific token IDs to fetch - only for build_playlist type. OPTIONAL: omit this field when user wants random tokens from the contract (e.g., "100 items from contract"). Only include when user specifies exact token IDs (e.g., "tokens 1, 2, 3").',
283
294
  items: {
284
295
  type: 'string',
285
296
  },
@@ -460,7 +471,7 @@ function formatMarkdown(text) {
460
471
  formatted = formatted.replace(/\*([^*]+)\*/g, (_, p1) => chalk_1.default.italic(p1));
461
472
  formatted = formatted.replace(/(?<!\w)_([^_]+)_(?!\w)/g, (_, p1) => chalk_1.default.italic(p1));
462
473
  // Inline code: `code` - light grey color
463
- formatted = formatted.replace(/`([^`]+)`/g, (_, p1) => chalk_1.default.grey(p1));
474
+ formatted = formatted.replace(/`([^`]+)`/g, (_, p1) => chalk_1.default.dim(p1));
464
475
  // Links: [text](url) - show text in blue
465
476
  formatted = formatted.replace(/\[([^\]]+)\]\([^)]+\)/g, (_, p1) => chalk_1.default.blue(p1));
466
477
  return formatted;
@@ -482,6 +493,20 @@ function printMarkdownContent(content) {
482
493
  function sleep(ms) {
483
494
  return new Promise((resolve) => setTimeout(resolve, ms));
484
495
  }
496
+ function buildToolResponseMessages(toolCalls, responses) {
497
+ return toolCalls
498
+ .filter((toolCall) => toolCall.id)
499
+ .map((toolCall) => {
500
+ const content = toolCall.id && Object.prototype.hasOwnProperty.call(responses, toolCall.id)
501
+ ? responses[toolCall.id]
502
+ : { error: `Unknown function: ${toolCall.function.name}` };
503
+ return {
504
+ role: 'tool',
505
+ tool_call_id: toolCall.id,
506
+ content: JSON.stringify(content),
507
+ };
508
+ });
509
+ }
485
510
  async function processNonStreamingResponse(response) {
486
511
  const message = response.choices[0]?.message;
487
512
  if (!message) {
@@ -680,13 +705,10 @@ async function processIntentParserRequest(userRequest, options = {}) {
680
705
  // Get the list of configured devices
681
706
  const { getConfiguredDevices } = await Promise.resolve().then(() => __importStar(require('../utilities/functions')));
682
707
  const result = await getConfiguredDevices();
683
- // Add tool result to messages and continue conversation
684
- const toolResultMessage = {
685
- role: 'tool',
686
- tool_call_id: toolCall.id,
687
- content: JSON.stringify(result),
688
- };
689
- const updatedMessages = [...messages, message, toolResultMessage];
708
+ const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
709
+ [toolCall.id]: result,
710
+ });
711
+ const updatedMessages = [...messages, message, ...toolResultMessages];
690
712
  // Continue the conversation with the device list
691
713
  const followUpRequest = {
692
714
  model: modelConfig.model,
@@ -730,16 +752,13 @@ async function processIntentParserRequest(userRequest, options = {}) {
730
752
  apiKey: server.apiKey,
731
753
  })) ||
732
754
  feedConfig.baseURLs.map((url) => ({ baseUrl: url, apiKey: feedConfig.apiKey }));
733
- // Add tool result to messages and continue conversation
734
- const feedToolResultMessage = {
735
- role: 'tool',
736
- tool_call_id: followUpToolCall.id,
737
- content: JSON.stringify({ servers: serverList }),
738
- };
755
+ const feedToolResultMessages = buildToolResponseMessages(followUpMessage.tool_calls, {
756
+ [followUpToolCall.id]: { servers: serverList },
757
+ });
739
758
  const feedUpdatedMessages = [
740
759
  ...updatedMessages,
741
760
  followUpMessage,
742
- feedToolResultMessage,
761
+ ...feedToolResultMessages,
743
762
  ];
744
763
  // Continue the conversation with the feed server list
745
764
  const feedFollowUpRequest = {
@@ -783,12 +802,12 @@ async function processIntentParserRequest(userRequest, options = {}) {
783
802
  console.log(chalk_1.default.cyan('Publishing to feed server...'));
784
803
  const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
785
804
  if (publishResult.success) {
786
- console.log(chalk_1.default.green('Published to feed server'));
805
+ console.log(chalk_1.default.green('Published to feed server'));
787
806
  if (publishResult.playlistId) {
788
- console.log(chalk_1.default.gray(` Playlist ID: ${publishResult.playlistId}`));
807
+ console.log(chalk_1.default.dim(` Playlist ID: ${publishResult.playlistId}`));
789
808
  }
790
809
  if (publishResult.feedServer) {
791
- console.log(chalk_1.default.gray(` Server: ${publishResult.feedServer}`));
810
+ console.log(chalk_1.default.dim(` Server: ${publishResult.feedServer}`));
792
811
  }
793
812
  console.log();
794
813
  return {
@@ -804,9 +823,9 @@ async function processIntentParserRequest(userRequest, options = {}) {
804
823
  };
805
824
  }
806
825
  else {
807
- console.error(chalk_1.default.red(' Failed to publish: ' + publishResult.error));
826
+ console.error(chalk_1.default.red('Publish failed: ' + publishResult.error));
808
827
  if (publishResult.message) {
809
- console.error(chalk_1.default.gray(` ${publishResult.message}`));
828
+ console.error(chalk_1.default.dim(` ${publishResult.message}`));
810
829
  }
811
830
  console.log();
812
831
  return {
@@ -837,12 +856,12 @@ async function processIntentParserRequest(userRequest, options = {}) {
837
856
  console.log(chalk_1.default.cyan('Publishing to feed server...'));
838
857
  const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
839
858
  if (publishResult.success) {
840
- console.log(chalk_1.default.green('Published to feed server'));
859
+ console.log(chalk_1.default.green('Published to feed server'));
841
860
  if (publishResult.playlistId) {
842
- console.log(chalk_1.default.gray(` Playlist ID: ${publishResult.playlistId}`));
861
+ console.log(chalk_1.default.dim(` Playlist ID: ${publishResult.playlistId}`));
843
862
  }
844
863
  if (publishResult.feedServer) {
845
- console.log(chalk_1.default.gray(` Server: ${publishResult.feedServer}`));
864
+ console.log(chalk_1.default.dim(` Server: ${publishResult.feedServer}`));
846
865
  }
847
866
  console.log();
848
867
  return {
@@ -858,9 +877,9 @@ async function processIntentParserRequest(userRequest, options = {}) {
858
877
  };
859
878
  }
860
879
  else {
861
- console.error(chalk_1.default.red(' Failed to publish: ' + publishResult.error));
880
+ console.error(chalk_1.default.red('Publish failed: ' + publishResult.error));
862
881
  if (publishResult.message) {
863
- console.error(chalk_1.default.gray(` ${publishResult.message}`));
882
+ console.error(chalk_1.default.dim(` ${publishResult.message}`));
864
883
  }
865
884
  console.log();
866
885
  return {
@@ -875,15 +894,8 @@ async function processIntentParserRequest(userRequest, options = {}) {
875
894
  }
876
895
  }
877
896
  else {
878
- // Unhandled tool call - add assistant message and tool response to messages
879
- const toolResultMessage = {
880
- role: 'tool',
881
- tool_call_id: followUpToolCall.id,
882
- content: JSON.stringify({
883
- error: `Unknown function: ${followUpToolCall.function.name}`,
884
- }),
885
- };
886
- const validMessages = [...updatedMessages, followUpMessage, toolResultMessage];
897
+ const toolResultMessages = buildToolResponseMessages(followUpMessage.tool_calls, {});
898
+ const validMessages = [...updatedMessages, followUpMessage, ...toolResultMessages];
887
899
  // AI is still asking for more information after the error
888
900
  return {
889
901
  approved: false,
@@ -910,13 +922,10 @@ async function processIntentParserRequest(userRequest, options = {}) {
910
922
  baseUrl: server.baseUrl,
911
923
  apiKey: server.apiKey,
912
924
  })) || feedConfig.baseURLs.map((url) => ({ baseUrl: url, apiKey: feedConfig.apiKey }));
913
- // Add tool result to messages and continue conversation
914
- const toolResultMessage = {
915
- role: 'tool',
916
- tool_call_id: toolCall.id,
917
- content: JSON.stringify({ servers: serverList }),
918
- };
919
- const updatedMessages = [...messages, message, toolResultMessage];
925
+ const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
926
+ [toolCall.id]: { servers: serverList },
927
+ });
928
+ const updatedMessages = [...messages, message, ...toolResultMessages];
920
929
  // Continue the conversation with the feed server list
921
930
  const followUpRequest = {
922
931
  model: modelConfig.model,
@@ -958,12 +967,12 @@ async function processIntentParserRequest(userRequest, options = {}) {
958
967
  console.log(chalk_1.default.cyan('Publishing to feed server...'));
959
968
  const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
960
969
  if (publishResult.success) {
961
- console.log(chalk_1.default.green('Published to feed server'));
970
+ console.log(chalk_1.default.green('Published to feed server'));
962
971
  if (publishResult.playlistId) {
963
- console.log(chalk_1.default.gray(` Playlist ID: ${publishResult.playlistId}`));
972
+ console.log(chalk_1.default.dim(` Playlist ID: ${publishResult.playlistId}`));
964
973
  }
965
974
  if (publishResult.feedServer) {
966
- console.log(chalk_1.default.gray(` Server: ${publishResult.feedServer}`));
975
+ console.log(chalk_1.default.dim(` Server: ${publishResult.feedServer}`));
967
976
  }
968
977
  console.log();
969
978
  return {
@@ -979,9 +988,9 @@ async function processIntentParserRequest(userRequest, options = {}) {
979
988
  };
980
989
  }
981
990
  else {
982
- console.error(chalk_1.default.red(' Failed to publish: ' + publishResult.error));
991
+ console.error(chalk_1.default.red('Publish failed: ' + publishResult.error));
983
992
  if (publishResult.message) {
984
- console.error(chalk_1.default.gray(` ${publishResult.message}`));
993
+ console.error(chalk_1.default.dim(` ${publishResult.message}`));
985
994
  }
986
995
  console.log();
987
996
  return {
@@ -1020,17 +1029,14 @@ async function processIntentParserRequest(userRequest, options = {}) {
1020
1029
  // Validate and confirm the playlist
1021
1030
  const confirmation = await confirmPlaylistForSending(args.filePath, args.deviceName);
1022
1031
  if (!confirmation.success) {
1023
- // Add tool response message to make conversation valid
1024
- const toolResultMessage = {
1025
- role: 'tool',
1026
- tool_call_id: toolCall.id,
1027
- content: JSON.stringify({
1032
+ const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
1033
+ [toolCall.id]: {
1028
1034
  success: false,
1029
1035
  error: confirmation.error,
1030
1036
  message: confirmation.message,
1031
- }),
1032
- };
1033
- const validMessages = [...messages, message, toolResultMessage];
1037
+ },
1038
+ });
1039
+ const validMessages = [...messages, message, ...toolResultMessages];
1034
1040
  // Check if this is a device selection needed case
1035
1041
  if (confirmation.needsDeviceSelection) {
1036
1042
  // Multiple devices available - ask user to choose
@@ -1072,12 +1078,12 @@ async function processIntentParserRequest(userRequest, options = {}) {
1072
1078
  console.log(chalk_1.default.cyan('Publishing to feed server...'));
1073
1079
  const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
1074
1080
  if (publishResult.success) {
1075
- console.log(chalk_1.default.green('Published to feed server'));
1081
+ console.log(chalk_1.default.green('Published to feed server'));
1076
1082
  if (publishResult.playlistId) {
1077
- console.log(chalk_1.default.gray(` Playlist ID: ${publishResult.playlistId}`));
1083
+ console.log(chalk_1.default.dim(` Playlist ID: ${publishResult.playlistId}`));
1078
1084
  }
1079
1085
  if (publishResult.feedServer) {
1080
- console.log(chalk_1.default.gray(` Server: ${publishResult.feedServer}`));
1086
+ console.log(chalk_1.default.dim(` Server: ${publishResult.feedServer}`));
1081
1087
  }
1082
1088
  console.log();
1083
1089
  return {
@@ -1093,9 +1099,9 @@ async function processIntentParserRequest(userRequest, options = {}) {
1093
1099
  };
1094
1100
  }
1095
1101
  else {
1096
- console.error(chalk_1.default.red(' Failed to publish: ' + publishResult.error));
1102
+ console.error(chalk_1.default.red('Publish failed: ' + publishResult.error));
1097
1103
  if (publishResult.message) {
1098
- console.error(chalk_1.default.gray(` ${publishResult.message}`));
1104
+ console.error(chalk_1.default.dim(` ${publishResult.message}`));
1099
1105
  }
1100
1106
  console.log();
1101
1107
  return {
@@ -1114,17 +1120,14 @@ async function processIntentParserRequest(userRequest, options = {}) {
1114
1120
  const { verifyAddresses } = await Promise.resolve().then(() => __importStar(require('../utilities/functions')));
1115
1121
  const verificationResult = await verifyAddresses({ addresses: args.addresses });
1116
1122
  if (!verificationResult.valid) {
1117
- // Add tool response message for invalid addresses
1118
- const toolResultMessage = {
1119
- role: 'tool',
1120
- tool_call_id: toolCall.id,
1121
- content: JSON.stringify({
1123
+ const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
1124
+ [toolCall.id]: {
1122
1125
  valid: false,
1123
1126
  errors: verificationResult.errors,
1124
1127
  results: verificationResult.results,
1125
- }),
1126
- };
1127
- const validMessages = [...messages, message, toolResultMessage];
1128
+ },
1129
+ });
1130
+ const validMessages = [...messages, message, ...toolResultMessages];
1128
1131
  // Ask user to correct the addresses
1129
1132
  return {
1130
1133
  approved: false,
@@ -1133,16 +1136,13 @@ async function processIntentParserRequest(userRequest, options = {}) {
1133
1136
  messages: validMessages,
1134
1137
  };
1135
1138
  }
1136
- // All addresses are valid - continue to next step
1137
- const toolResultMessage = {
1138
- role: 'tool',
1139
- tool_call_id: toolCall.id,
1140
- content: JSON.stringify({
1139
+ const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
1140
+ [toolCall.id]: {
1141
1141
  valid: true,
1142
1142
  results: verificationResult.results,
1143
- }),
1144
- };
1145
- const validMessages = [...messages, message, toolResultMessage];
1143
+ },
1144
+ });
1145
+ const validMessages = [...messages, message, ...toolResultMessages];
1146
1146
  // Continue conversation after validation
1147
1147
  const followUpRequest = {
1148
1148
  model: modelConfig.model,
@@ -1187,12 +1187,14 @@ async function processIntentParserRequest(userRequest, options = {}) {
1187
1187
  })) ||
1188
1188
  feedConfig.baseURLs.map((url) => ({ baseUrl: url, apiKey: feedConfig.apiKey }));
1189
1189
  // Add tool result to messages and continue conversation
1190
- const feedToolResultMessage = {
1191
- role: 'tool',
1192
- tool_call_id: followUpToolCall.id,
1193
- content: JSON.stringify({ servers: serverList }),
1194
- };
1195
- const feedUpdatedMessages = [...validMessages, followUpMessage, feedToolResultMessage];
1190
+ const feedToolResultMessages = buildToolResponseMessages(followUpMessage.tool_calls, {
1191
+ [followUpToolCall.id]: { servers: serverList },
1192
+ });
1193
+ const feedUpdatedMessages = [
1194
+ ...validMessages,
1195
+ followUpMessage,
1196
+ ...feedToolResultMessages,
1197
+ ];
1196
1198
  // Continue the conversation with the feed server list
1197
1199
  const feedFollowUpRequest = {
1198
1200
  model: modelConfig.model,
@@ -1235,12 +1237,12 @@ async function processIntentParserRequest(userRequest, options = {}) {
1235
1237
  console.log(chalk_1.default.cyan('Publishing to feed server...'));
1236
1238
  const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
1237
1239
  if (publishResult.success) {
1238
- console.log(chalk_1.default.green('Published to feed server'));
1240
+ console.log(chalk_1.default.green('Published to feed server'));
1239
1241
  if (publishResult.playlistId) {
1240
- console.log(chalk_1.default.gray(` Playlist ID: ${publishResult.playlistId}`));
1242
+ console.log(chalk_1.default.dim(` Playlist ID: ${publishResult.playlistId}`));
1241
1243
  }
1242
1244
  if (publishResult.feedServer) {
1243
- console.log(chalk_1.default.gray(` Server: ${publishResult.feedServer}`));
1245
+ console.log(chalk_1.default.dim(` Server: ${publishResult.feedServer}`));
1244
1246
  }
1245
1247
  console.log();
1246
1248
  return {
@@ -1256,9 +1258,9 @@ async function processIntentParserRequest(userRequest, options = {}) {
1256
1258
  };
1257
1259
  }
1258
1260
  else {
1259
- console.error(chalk_1.default.red(' Failed to publish: ' + publishResult.error));
1261
+ console.error(chalk_1.default.red('Publish failed: ' + publishResult.error));
1260
1262
  if (publishResult.message) {
1261
- console.error(chalk_1.default.gray(` ${publishResult.message}`));
1263
+ console.error(chalk_1.default.dim(` ${publishResult.message}`));
1262
1264
  }
1263
1265
  console.log();
1264
1266
  return {
@@ -1297,15 +1299,8 @@ async function processIntentParserRequest(userRequest, options = {}) {
1297
1299
  };
1298
1300
  }
1299
1301
  else {
1300
- // Unhandled tool call at top level
1301
- const toolResultMessage = {
1302
- role: 'tool',
1303
- tool_call_id: toolCall.id,
1304
- content: JSON.stringify({
1305
- error: `Unknown function: ${toolCall.function.name}`,
1306
- }),
1307
- };
1308
- const validMessages = [...messages, message, toolResultMessage];
1302
+ const toolResultMessages = buildToolResponseMessages(message.tool_calls, {});
1303
+ const validMessages = [...messages, message, ...toolResultMessages];
1309
1304
  return {
1310
1305
  approved: false,
1311
1306
  needsMoreInfo: true,
@@ -40,9 +40,6 @@ function applyConstraints(params, config) {
40
40
  if (!r.contractAddress) {
41
41
  throw new Error(`Requirement ${index + 1}: contractAddress is required for build_playlist`);
42
42
  }
43
- if (!r.tokenIds || r.tokenIds.length === 0) {
44
- throw new Error(`Requirement ${index + 1}: tokenIds are required for build_playlist`);
45
- }
46
43
  }
47
44
  else if (r.type === 'query_address') {
48
45
  if (!r.ownerAddress) {
@@ -87,10 +84,10 @@ function applyConstraints(params, config) {
87
84
  return sum;
88
85
  }, 0);
89
86
  if (hasAllQuantity) {
90
- console.log(chalk_1.default.yellow(`\n⚠️ Requesting all tokens from one or more addresses. This may take a while to fetch and process...\n`));
87
+ console.log(chalk_1.default.yellow(`\nRequesting all tokens from one or more addresses. This may take a while to fetch and process.\n`));
91
88
  }
92
89
  else if (totalRequested > 100) {
93
- console.log(chalk_1.default.yellow(`\n⚠️ Requesting ${totalRequested} items. This may take a while to fetch and process...\n`));
90
+ console.log(chalk_1.default.yellow(`\nRequesting ${totalRequested} items. This may take a while to fetch and process.\n`));
94
91
  }
95
92
  // Set playlist defaults
96
93
  if (!params.playlistSettings) {
@@ -28,7 +28,7 @@ function setVerbose(verbose) {
28
28
  */
29
29
  function debug(...args) {
30
30
  if (isVerbose) {
31
- console.log(chalk_1.default.gray('[DEBUG]'), ...args);
31
+ console.log(chalk_1.default.dim('[DEBUG]'), ...args);
32
32
  }
33
33
  }
34
34
  /**
package/dist/src/main.js CHANGED
@@ -116,17 +116,30 @@ function validateRequirements(requirements) {
116
116
  if (!req.blockchain) {
117
117
  throw new Error(`Requirement ${index + 1}: blockchain is required for build_playlist`);
118
118
  }
119
- if (!req.tokenIds || req.tokenIds.length === 0) {
120
- throw new Error(`Requirement ${index + 1}: at least one token ID is required`);
119
+ if (!req.contractAddress) {
120
+ throw new Error(`Requirement ${index + 1}: contractAddress is required for build_playlist`);
121
+ }
122
+ // tokenIds is now optional - if not provided, query random tokens from contract
123
+ if (req.tokenIds && req.tokenIds.length > 0) {
124
+ // Specific token IDs provided
125
+ const quantity = typeof req.quantity === 'number'
126
+ ? Math.min(req.quantity, 20)
127
+ : Math.min(req.tokenIds.length, 20);
128
+ return {
129
+ ...req,
130
+ quantity,
131
+ tokenIds: req.tokenIds,
132
+ };
133
+ }
134
+ else {
135
+ // No token IDs - query random tokens from contract
136
+ const quantity = typeof req.quantity === 'number' ? Math.min(req.quantity, 100) : 100;
137
+ return {
138
+ ...req,
139
+ quantity,
140
+ tokenIds: undefined, // Explicitly set to undefined
141
+ };
121
142
  }
122
- const quantity = typeof req.quantity === 'number'
123
- ? Math.min(req.quantity, 20)
124
- : Math.min(req.tokenIds.length, 20);
125
- return {
126
- ...req,
127
- quantity,
128
- tokenIds: req.tokenIds || [],
129
- };
130
143
  }
131
144
  throw new Error(`Requirement ${index + 1}: invalid type "${req.type}"`);
132
145
  });
@@ -210,9 +223,9 @@ async function buildPlaylist(userRequest, options = {}) {
210
223
  }
211
224
  const sendResult = await utilities.sendToDevice(confirmation.playlist, confirmation.deviceName);
212
225
  if (sendResult.success) {
213
- console.log(chalk_1.default.green('\n✅ Playlist sent successfully!'));
226
+ console.log(chalk_1.default.green('\nPlaylist sent'));
214
227
  if (sendResult.deviceName) {
215
- console.log(chalk_1.default.gray(` Device: ${sendResult.deviceName}`));
228
+ console.log(chalk_1.default.dim(` Device: ${sendResult.deviceName}`));
216
229
  }
217
230
  console.log();
218
231
  return {
@@ -222,9 +235,9 @@ async function buildPlaylist(userRequest, options = {}) {
222
235
  };
223
236
  }
224
237
  console.log();
225
- console.error(chalk_1.default.red(' Failed to send playlist'));
238
+ console.error(chalk_1.default.red('Send failed'));
226
239
  if (sendResult.error) {
227
- console.error(chalk_1.default.red(` ${sendResult.error}`));
240
+ console.error(chalk_1.default.red(` ${sendResult.error}`));
228
241
  }
229
242
  return {
230
243
  success: false,
@@ -243,9 +256,9 @@ async function buildPlaylist(userRequest, options = {}) {
243
256
  while (intentParserResult.needsMoreInfo) {
244
257
  if (!interactive) {
245
258
  // Non-interactive mode: cannot ask for clarification
246
- console.error(chalk_1.default.red('\n❌ More information needed but running in non-interactive mode. Please provide a complete request.'));
259
+ console.error(chalk_1.default.red('\nNeed more information, but running in non-interactive mode. Provide a complete request.'));
247
260
  if (intentParserResult.question) {
248
- console.error(chalk_1.default.yellow('\nAI asked: ') + intentParserResult.question);
261
+ console.error(chalk_1.default.yellow('\nQuestion: ') + intentParserResult.question);
249
262
  }
250
263
  process.exit(1);
251
264
  }
@@ -256,7 +269,7 @@ async function buildPlaylist(userRequest, options = {}) {
256
269
  });
257
270
  // Display the AI's question before asking for input
258
271
  if (intentParserResult.question) {
259
- console.log(chalk_1.default.cyan('\n🤖 ') + intentParserResult.question);
272
+ console.log(chalk_1.default.cyan('\n') + intentParserResult.question);
260
273
  }
261
274
  const userResponse = await new Promise((resolve) => {
262
275
  rl.question(chalk_1.default.yellow('Your response: '), (answer) => {
@@ -265,7 +278,7 @@ async function buildPlaylist(userRequest, options = {}) {
265
278
  });
266
279
  });
267
280
  if (!userResponse) {
268
- console.error(chalk_1.default.red('\n❌ No response provided. Exiting.'));
281
+ console.error(chalk_1.default.red('\nNo response provided. Exiting.'));
269
282
  process.exit(1);
270
283
  }
271
284
  console.log();
@@ -278,7 +291,7 @@ async function buildPlaylist(userRequest, options = {}) {
278
291
  });
279
292
  }
280
293
  if (!intentParserResult.approved) {
281
- console.error(chalk_1.default.red('\n❌ Request not approved by intent parser'));
294
+ console.error(chalk_1.default.red('\nRequest not approved by intent parser'));
282
295
  return null;
283
296
  }
284
297
  const params = intentParserResult.params;
@@ -288,12 +301,12 @@ async function buildPlaylist(userRequest, options = {}) {
288
301
  const sendParams = params;
289
302
  const utilities = getUtilities();
290
303
  console.log();
291
- console.log(chalk_1.default.cyan('Sending to device...'));
304
+ console.log(chalk_1.default.cyan('Sending to device'));
292
305
  const sendResult = await utilities.sendToDevice(sendParams.playlist, sendParams.deviceName);
293
306
  if (sendResult.success) {
294
- console.log(chalk_1.default.green('\n✅ Playlist sent successfully!'));
307
+ console.log(chalk_1.default.green('\nPlaylist sent'));
295
308
  if (sendResult.deviceName) {
296
- console.log(chalk_1.default.gray(` Device: ${sendResult.deviceName}`));
309
+ console.log(chalk_1.default.dim(` Device: ${sendResult.deviceName}`));
297
310
  }
298
311
  console.log();
299
312
  return {
@@ -305,9 +318,9 @@ async function buildPlaylist(userRequest, options = {}) {
305
318
  else {
306
319
  // Send failed - return error without showing the playlist summary
307
320
  console.log();
308
- console.error(chalk_1.default.red(' Failed to send playlist'));
321
+ console.error(chalk_1.default.red('Send failed'));
309
322
  if (sendResult.error) {
310
- console.error(chalk_1.default.red(` ${sendResult.error}`));
323
+ console.error(chalk_1.default.red(` ${sendResult.error}`));
311
324
  }
312
325
  return {
313
326
  success: false,
@@ -361,7 +374,7 @@ async function buildPlaylist(userRequest, options = {}) {
361
374
  });
362
375
  });
363
376
  if (!userResponse) {
364
- console.error(chalk_1.default.red('\n❌ No response provided. Canceling.'));
377
+ console.error(chalk_1.default.red('\nNo response provided. Canceling.'));
365
378
  return null;
366
379
  }
367
380
  console.log();
@@ -379,14 +392,14 @@ async function buildPlaylist(userRequest, options = {}) {
379
392
  }
380
393
  // If no playlist was built, display the AI's message
381
394
  if (!result.playlist && result.message) {
382
- console.log(chalk_1.default.yellow('\n⚠️ ' + result.message));
395
+ console.log(chalk_1.default.yellow('\n' + result.message));
383
396
  }
384
397
  return result;
385
398
  }
386
399
  catch (error) {
387
- console.error(chalk_1.default.red('\n❌ Error:'), error.message);
400
+ console.error(chalk_1.default.red('\nError:'), error.message);
388
401
  if (verbose) {
389
- console.error(chalk_1.default.gray(error.stack));
402
+ console.error(chalk_1.default.dim(error.stack));
390
403
  }
391
404
  throw error;
392
405
  }