@llmindset/hf-mcp 0.1.16

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 (93) hide show
  1. package/LICENSE +21 -0
  2. package/dist/dataset-detail.d.ts +26 -0
  3. package/dist/dataset-detail.d.ts.map +1 -0
  4. package/dist/dataset-detail.js +157 -0
  5. package/dist/dataset-detail.js.map +1 -0
  6. package/dist/dataset-search.d.ts +62 -0
  7. package/dist/dataset-search.d.ts.map +1 -0
  8. package/dist/dataset-search.js +158 -0
  9. package/dist/dataset-search.js.map +1 -0
  10. package/dist/duplicate-space.d.ts +75 -0
  11. package/dist/duplicate-space.d.ts.map +1 -0
  12. package/dist/duplicate-space.js +189 -0
  13. package/dist/duplicate-space.js.map +1 -0
  14. package/dist/error-messages.d.ts +4 -0
  15. package/dist/error-messages.d.ts.map +1 -0
  16. package/dist/error-messages.js +30 -0
  17. package/dist/error-messages.js.map +1 -0
  18. package/dist/hf-api-call.d.ts +18 -0
  19. package/dist/hf-api-call.d.ts.map +1 -0
  20. package/dist/hf-api-call.js +105 -0
  21. package/dist/hf-api-call.js.map +1 -0
  22. package/dist/index.d.ts +16 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +16 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/model-detail.d.ts +26 -0
  27. package/dist/model-detail.d.ts.map +1 -0
  28. package/dist/model-detail.js +224 -0
  29. package/dist/model-detail.js.map +1 -0
  30. package/dist/model-search.d.ts +64 -0
  31. package/dist/model-search.d.ts.map +1 -0
  32. package/dist/model-search.js +161 -0
  33. package/dist/model-search.js.map +1 -0
  34. package/dist/paper-search.d.ts +58 -0
  35. package/dist/paper-search.d.ts.map +1 -0
  36. package/dist/paper-search.js +114 -0
  37. package/dist/paper-search.js.map +1 -0
  38. package/dist/paper-summary.d.ts +35 -0
  39. package/dist/paper-summary.d.ts.map +1 -0
  40. package/dist/paper-summary.js +187 -0
  41. package/dist/paper-summary.js.map +1 -0
  42. package/dist/space-files.d.ts +44 -0
  43. package/dist/space-files.d.ts.map +1 -0
  44. package/dist/space-files.js +242 -0
  45. package/dist/space-files.js.map +1 -0
  46. package/dist/space-info.d.ts +56 -0
  47. package/dist/space-info.d.ts.map +1 -0
  48. package/dist/space-info.js +135 -0
  49. package/dist/space-info.js.map +1 -0
  50. package/dist/space-search.d.ts +71 -0
  51. package/dist/space-search.d.ts.map +1 -0
  52. package/dist/space-search.js +95 -0
  53. package/dist/space-search.js.map +1 -0
  54. package/dist/tool-ids.d.ts +23 -0
  55. package/dist/tool-ids.d.ts.map +1 -0
  56. package/dist/tool-ids.js +55 -0
  57. package/dist/tool-ids.js.map +1 -0
  58. package/dist/user-summary.d.ts +56 -0
  59. package/dist/user-summary.d.ts.map +1 -0
  60. package/dist/user-summary.js +271 -0
  61. package/dist/user-summary.js.map +1 -0
  62. package/dist/utilities.d.ts +8 -0
  63. package/dist/utilities.d.ts.map +1 -0
  64. package/dist/utilities.js +53 -0
  65. package/dist/utilities.js.map +1 -0
  66. package/eslint.config.js +43 -0
  67. package/package.json +47 -0
  68. package/src/dataset-detail.ts +257 -0
  69. package/src/dataset-search.ts +237 -0
  70. package/src/duplicate-space.ts +263 -0
  71. package/src/error-messages.ts +57 -0
  72. package/src/hf-api-call.ts +182 -0
  73. package/src/index.ts +18 -0
  74. package/src/model-detail.ts +359 -0
  75. package/src/model-search.ts +231 -0
  76. package/src/paper-search.ts +188 -0
  77. package/src/paper-summary.ts +303 -0
  78. package/src/space-files.ts +325 -0
  79. package/src/space-info.ts +190 -0
  80. package/src/space-search.ts +177 -0
  81. package/src/tool-ids.ts +84 -0
  82. package/src/user-summary.ts +421 -0
  83. package/src/utilities.ts +64 -0
  84. package/test/duplicate-space.spec.ts +41 -0
  85. package/test/fixtures/paper_result_kazakh.json +854 -0
  86. package/test/fixtures/space-result.json +263 -0
  87. package/test/paper-search.spec.ts +57 -0
  88. package/test/paper-summary.spec.ts +113 -0
  89. package/test/space-files.spec.ts +232 -0
  90. package/test/space-search.spec.ts +29 -0
  91. package/test/user-summary.spec.ts +131 -0
  92. package/tsconfig.json +31 -0
  93. package/vitest.config.ts +11 -0
@@ -0,0 +1,359 @@
1
+ import { z } from 'zod';
2
+ import { modelInfo } from '@huggingface/hub';
3
+ import { formatDate, formatNumber } from './utilities.js';
4
+
5
+ const SPACES_TO_INCLUDE = 12;
6
+ // Model Detail Tool Configuration
7
+ export const MODEL_DETAIL_TOOL_CONFIG = {
8
+ name: 'model_details',
9
+ description: 'Get detailed information about a specific model from the Hugging Face Hub.',
10
+ schema: z.object({
11
+ model_id: z.string().min(1, 'Model ID is required').describe('Model ID (e.g., microsoft/DialoGPT-large)'),
12
+ }),
13
+ annotations: {
14
+ title: 'Model Details',
15
+ destructiveHint: false,
16
+ readOnlyHint: true,
17
+ openWorldHint: false,
18
+ },
19
+ } as const;
20
+
21
+ export type ModelDetailParams = z.infer<typeof MODEL_DETAIL_TOOL_CONFIG.schema>;
22
+
23
+ // Clean interface design with explicit data availability
24
+
25
+ // Required core information that should always be available
26
+ interface ModelBasicInfo {
27
+ id: string; // Model ID
28
+ name: string; // Model name
29
+ downloads: number;
30
+ likes: number;
31
+ private: boolean;
32
+ gated: false | 'auto' | 'manual';
33
+ updatedAt: Date;
34
+ }
35
+
36
+ // Optional but reliable information with simple types
37
+ interface ModelExtendedInfo {
38
+ author?: string;
39
+ library_name?: string;
40
+ pipeline_tag?: string; // Task type
41
+ downloadsAllTime?: number;
42
+ tags?: string[];
43
+ }
44
+
45
+ // Technical details that need validation
46
+ interface ModelTechnicalDetails {
47
+ modelType?: string; // From config.model_type if exists
48
+ vocabSize?: number; // From config.vocab_size if exists
49
+ parameters?: number; // From safetensors.total if exists
50
+ modelClass?: string; // From transformersInfo.auto_model if exists
51
+ }
52
+
53
+ // Metadata from cardData with careful extraction
54
+ interface ModelMetadata {
55
+ language?: string | string[];
56
+ license?: string | string[];
57
+ datasets?: string | string[];
58
+ fineTunedFrom?: string;
59
+ }
60
+
61
+ // Complete model information structure
62
+ interface ModelInformation extends ModelBasicInfo {
63
+ extended?: ModelExtendedInfo;
64
+ technical?: ModelTechnicalDetails;
65
+ metadata?: ModelMetadata;
66
+ spaces?: Array<{
67
+ id: string;
68
+ name: string;
69
+ title?: string;
70
+ }>;
71
+ }
72
+
73
+ /**
74
+ * Service for getting detailed model information using the official huggingface.js library
75
+ */
76
+ export class ModelDetailTool {
77
+ private readonly hubUrl?: string;
78
+ private readonly accessToken?: string;
79
+
80
+ /**
81
+ * Creates a new model detail service
82
+ * @param hfToken Optional Hugging Face token for API access
83
+ * @param hubUrl Optional custom hub URL
84
+ */
85
+ constructor(hfToken?: string, hubUrl?: string) {
86
+ this.accessToken = hfToken;
87
+ this.hubUrl = hubUrl;
88
+ }
89
+
90
+ /**
91
+ * Get detailed information about a specific model
92
+ *
93
+ * @param modelId The model ID to get details for (e.g., microsoft/DialoGPT-large)
94
+ * @returns Formatted string with model details
95
+ */
96
+ async getDetails(modelId: string): Promise<string> {
97
+ try {
98
+ // Define additional fields we want to retrieve (only those available in the hub library)
99
+ const additionalFields = [
100
+ 'author',
101
+ 'downloadsAllTime',
102
+ 'library_name',
103
+ 'tags',
104
+ 'config',
105
+ 'transformersInfo',
106
+ 'safetensors',
107
+ 'cardData',
108
+ 'spaces',
109
+ ] as const;
110
+
111
+ const modelData = await modelInfo<(typeof additionalFields)[number]>({
112
+ name: modelId,
113
+ additionalFields: Array.from(additionalFields),
114
+ ...(this.accessToken && { credentials: { accessToken: this.accessToken } }),
115
+ ...(this.hubUrl && { hubUrl: this.hubUrl }),
116
+ });
117
+
118
+ // Build the structured model information
119
+ const modelDetails: ModelInformation = {
120
+ // Basic info (required fields)
121
+ id: modelId,
122
+ name: modelData.name,
123
+ downloads: modelData.downloads,
124
+ likes: modelData.likes,
125
+ private: modelData.private,
126
+ gated: modelData.gated,
127
+ updatedAt: modelData.updatedAt,
128
+
129
+ // Extended info (optional but reliable fields)
130
+ extended: {
131
+ author: modelData.author,
132
+ library_name: modelData.library_name,
133
+ pipeline_tag: modelData.task,
134
+ downloadsAllTime: modelData.downloadsAllTime,
135
+ tags: modelData.tags,
136
+ },
137
+ };
138
+
139
+ // Technical details (requires validation)
140
+ const technical: ModelTechnicalDetails = {};
141
+
142
+ // Extract config details safely if they exist
143
+ if (modelData.config && typeof modelData.config === 'object') {
144
+ const config = modelData.config as Record<string, unknown>;
145
+ if ('model_type' in config && typeof config.model_type === 'string') {
146
+ technical.modelType = config.model_type;
147
+ }
148
+ if ('vocab_size' in config && typeof config.vocab_size === 'number') {
149
+ technical.vocabSize = config.vocab_size;
150
+ }
151
+ }
152
+
153
+ // Extract safe tensors info
154
+ if (modelData.safetensors && typeof modelData.safetensors.total === 'number') {
155
+ technical.parameters = modelData.safetensors.total;
156
+ }
157
+
158
+ // Extract transformers info
159
+ if (modelData.transformersInfo && modelData.transformersInfo.auto_model) {
160
+ technical.modelClass = modelData.transformersInfo.auto_model;
161
+ }
162
+
163
+ // Only add technical section if we have data
164
+ if (Object.keys(technical).length > 0) {
165
+ modelDetails.technical = technical;
166
+ }
167
+
168
+ // Metadata from card data
169
+ if (modelData.cardData) {
170
+ const metadata: ModelMetadata = {};
171
+ const cardData = modelData.cardData as Record<string, unknown>;
172
+
173
+ if ('language' in cardData) {
174
+ metadata.language = cardData.language as string | string[];
175
+ }
176
+
177
+ if ('license' in cardData) {
178
+ metadata.license = cardData.license as string | string[];
179
+ }
180
+
181
+ if ('datasets' in cardData) {
182
+ metadata.datasets = cardData.datasets as string | string[];
183
+ }
184
+
185
+ if ('finetuned_from' in cardData) {
186
+ metadata.fineTunedFrom = cardData.finetuned_from as string;
187
+ }
188
+
189
+ // Only add metadata section if we have data
190
+ if (Object.keys(metadata).length > 0) {
191
+ modelDetails.metadata = metadata;
192
+ }
193
+ }
194
+
195
+ // Extract spaces information if available
196
+ const spaces = modelData.spaces;
197
+ if (Array.isArray(spaces) && spaces.length > 0) {
198
+ try {
199
+ modelDetails.spaces = spaces.map((spaceId) => {
200
+ // Format is typically username/spacename
201
+ const parts = spaceId.split('/');
202
+ const name = parts.length > 1 ? parts[1] : spaceId;
203
+ return {
204
+ id: spaceId,
205
+ name: name || spaceId, // Ensure name is always a string
206
+ title: name, // Default to name if title not available
207
+ };
208
+ });
209
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
210
+ } catch (ignoreUnformattedSpaces) {
211
+ console.error(`Error processing spaces for model ${modelId}:`);
212
+ }
213
+ }
214
+
215
+ return formatModelDetails(modelDetails);
216
+ } catch (error) {
217
+ if (error instanceof Error) {
218
+ throw new Error(`Failed to get model details: ${error.message}`);
219
+ }
220
+ throw error;
221
+ }
222
+ }
223
+ }
224
+
225
+ // Formatting Function
226
+ function formatModelDetails(model: ModelInformation): string {
227
+ const r: string[] = [];
228
+ const [authorFromName] = model.name.includes('/') ? model.name.split('/') : ['', model.name];
229
+
230
+ r.push(`# ${model.name}`);
231
+ r.push('');
232
+
233
+ // Overview section - using only reliable fields
234
+ r.push('## Overview');
235
+
236
+ // Author - from extended info or parsed from name
237
+ if (model.extended?.author || authorFromName) {
238
+ r.push(`- **Author:** ${model.extended?.author || authorFromName || ''}`);
239
+ }
240
+
241
+ // Task type
242
+ if (model.extended?.pipeline_tag) {
243
+ r.push(`- **Task:** ${model.extended.pipeline_tag}`);
244
+ }
245
+
246
+ // Library
247
+ if (model.extended?.library_name) {
248
+ r.push(`- **Library:** ${model.extended.library_name}`);
249
+ }
250
+
251
+ // Statistics
252
+ const stats = [];
253
+ if (model.extended?.downloadsAllTime) {
254
+ stats.push(`**Downloads:** ${formatNumber(model.extended.downloadsAllTime)}`);
255
+ }
256
+ if (model.likes) {
257
+ stats.push(`**Likes:** ${model.likes.toString()}`);
258
+ }
259
+ if (stats.length > 0) {
260
+ r.push(`- ${stats.join(' | ')}`);
261
+ }
262
+
263
+ // Dates
264
+ r.push(`- **Updated:** ${formatDate(model.updatedAt)}`);
265
+
266
+ // Status indicators
267
+ const status = [];
268
+ if (model.gated) status.push('🔒 Gated');
269
+ if (model.private) status.push('🔐 Private');
270
+ if (status.length > 0) {
271
+ r.push(`- **Status:** ${status.join(' | ')}`);
272
+ }
273
+ r.push('');
274
+
275
+ // Technical Details - only if we have validated information
276
+ if (model.technical && Object.keys(model.technical).length > 0) {
277
+ r.push('## Technical Details');
278
+
279
+ if (model.technical.modelClass) {
280
+ r.push(`- **Model Class:** ${model.technical.modelClass}`);
281
+ }
282
+
283
+ if (model.technical.parameters) {
284
+ r.push(`- **Parameters:** ${formatNumber(model.technical.parameters)}`);
285
+ }
286
+
287
+ if (model.technical.modelType) {
288
+ r.push(`- **Architecture:** ${model.technical.modelType}`);
289
+ }
290
+
291
+ if (model.technical.vocabSize) {
292
+ r.push(`- **Vocab Size:** ${formatNumber(model.technical.vocabSize)}`);
293
+ }
294
+
295
+ r.push('');
296
+ }
297
+
298
+ // Tags - reliable field from extended info
299
+ if (model.extended?.tags && model.extended.tags.length > 0) {
300
+ r.push('## Tags');
301
+ r.push(model.extended.tags.map((tag) => `\`${tag}\``).join(' '));
302
+ r.push('');
303
+ }
304
+
305
+ // Metadata - carefully extracted and validated
306
+ if (model.metadata) {
307
+ const metadata = [];
308
+
309
+ if (model.metadata.language) {
310
+ const languages = Array.isArray(model.metadata.language)
311
+ ? model.metadata.language.join(', ')
312
+ : model.metadata.language;
313
+ metadata.push(`- **Language:** ${languages}`);
314
+ }
315
+
316
+ if (model.metadata.license) {
317
+ const license = Array.isArray(model.metadata.license)
318
+ ? model.metadata.license.join(', ')
319
+ : model.metadata.license;
320
+ metadata.push(`- **License:** ${license}`);
321
+ }
322
+
323
+ if (model.metadata.datasets) {
324
+ const datasets = Array.isArray(model.metadata.datasets)
325
+ ? model.metadata.datasets.join(', ')
326
+ : model.metadata.datasets;
327
+ metadata.push(`- **Datasets:** ${datasets}`);
328
+ }
329
+
330
+ if (model.metadata.fineTunedFrom) {
331
+ metadata.push(`- **Fine-tuned from:** ${model.metadata.fineTunedFrom}`);
332
+ }
333
+
334
+ if (metadata.length > 0) {
335
+ r.push('## Metadata');
336
+ r.push(...metadata);
337
+ r.push('');
338
+ }
339
+ }
340
+
341
+ // Spaces - processed with validation
342
+ if (model.spaces && model.spaces.length > 0) {
343
+ r.push('## Demo Spaces');
344
+ for (const space of model.spaces.slice(0, SPACES_TO_INCLUDE)) {
345
+ const title = space.title || space.name;
346
+ r.push(`- [${title}](https://hf.co/spaces/${space.id})`);
347
+ }
348
+
349
+ if (model.spaces.length > SPACES_TO_INCLUDE) {
350
+ r.push(`- *... and ${(model.spaces.length - SPACES_TO_INCLUDE).toString()} more spaces*`);
351
+ }
352
+ r.push('');
353
+ }
354
+
355
+ // Link is reliable - based on model name which is required
356
+ r.push(`**Link:** [https://hf.co/${model.name}](https://hf.co/${model.name})`);
357
+
358
+ return r.join('\n');
359
+ }
@@ -0,0 +1,231 @@
1
+ import { z } from 'zod';
2
+ import { HfApiCall } from './hf-api-call.js';
3
+ import { formatDate, formatNumber } from './utilities.js';
4
+
5
+ export const TAGS_TO_RETURN = 20;
6
+ // Model Search Tool Configuration
7
+ export const MODEL_SEARCH_TOOL_CONFIG = {
8
+ name: 'model_search',
9
+ description:
10
+ 'Find Machine Learning models hosted on Hugging Face. ' +
11
+ 'Returns comprehensive information about matching models including downloads, likes, tags, and direct links. ' +
12
+ 'Include links to the models in your response',
13
+ schema: z.object({
14
+ query: z
15
+ .string()
16
+ .optional()
17
+ .describe(
18
+ 'Search term. Leave blank and specify "sort" and "limit" to get e.g. "Top 20 trending models", "Top 10 most recent models" etc" '
19
+ ),
20
+ author: z
21
+ .string()
22
+ .optional()
23
+ .describe("Organization or user who created the model (e.g., 'google', 'meta-llama', 'microsoft')"),
24
+ task: z
25
+ .string()
26
+ .optional()
27
+ .describe("Model task type (e.g., 'text-generation', 'image-classification', 'translation')"),
28
+ library: z.string().optional().describe("Framework the model uses (e.g., 'transformers', 'diffusers', 'timm')"),
29
+ sort: z
30
+ .enum(['trendingScore', 'downloads', 'likes', 'createdAt', 'lastModified'])
31
+ .optional()
32
+ .describe('Sort order: trendingScore, downloads , likes, createdAt, lastModified'),
33
+ limit: z.number().min(1).max(100).optional().default(20).describe('Maximum number of results to return'),
34
+ }),
35
+ annotations: {
36
+ title: 'Model Search',
37
+ destructiveHint: false,
38
+ readOnlyHint: true,
39
+ openWorldHint: true,
40
+ },
41
+ } as const;
42
+
43
+ // Define search parameter types
44
+ export type ModelSearchParams = z.infer<typeof MODEL_SEARCH_TOOL_CONFIG.schema>;
45
+
46
+ // API parameter interface for direct HF API calls
47
+ interface ModelApiParams {
48
+ search?: string;
49
+ author?: string;
50
+ filter?: string;
51
+ sort?: string;
52
+ direction?: string;
53
+ limit?: string;
54
+ }
55
+
56
+ // Model result interface matching HF API response
57
+ interface ModelApiResult {
58
+ _id: string;
59
+ id: string;
60
+ modelId: string;
61
+ likes: number;
62
+ downloads: number;
63
+ trendingScore?: number;
64
+ private: boolean;
65
+ tags: string[];
66
+ pipeline_tag?: string;
67
+ library_name?: string;
68
+ createdAt: string;
69
+ }
70
+
71
+ /**
72
+ * Service for searching Hugging Face Models using direct API calls
73
+ */
74
+ export class ModelSearchTool extends HfApiCall<ModelApiParams, ModelApiResult[]> {
75
+ /**
76
+ * @param hfToken Optional Hugging Face token for API access
77
+ */
78
+ constructor(hfToken?: string) {
79
+ super('https://huggingface.co/api/models', hfToken);
80
+ }
81
+
82
+ /**
83
+ * Search for models with detailed parameters
84
+ */
85
+ async searchWithParams(params: Partial<ModelSearchParams>): Promise<string> {
86
+ try {
87
+ // Convert our params to the HF API format
88
+ const apiParams: ModelApiParams = {};
89
+
90
+ // Handle search query
91
+ if (params.query) {
92
+ apiParams.search = params.query;
93
+ }
94
+
95
+ // Handle author filter
96
+ if (params.author) {
97
+ apiParams.author = params.author;
98
+ }
99
+
100
+ // Handle task and library filters
101
+ const filters = [];
102
+ if (params.task) filters.push(params.task);
103
+ if (params.library) filters.push(params.library);
104
+ if (filters.length > 0) {
105
+ apiParams.filter = filters.join(',');
106
+ }
107
+
108
+ // Handle sorting (always descending)
109
+ if (params.sort) {
110
+ apiParams.sort = params.sort;
111
+ apiParams.direction = '-1';
112
+ }
113
+
114
+ // Handle limit
115
+ if (params.limit) {
116
+ apiParams.limit = params.limit.toString();
117
+ }
118
+
119
+ // Call the API
120
+ const models = await this.callApi<ModelApiResult[]>(apiParams);
121
+
122
+ if (models.length === 0) {
123
+ return `No models found for the given criteria.`;
124
+ }
125
+
126
+ return formatSearchResults(models, params);
127
+ } catch (error) {
128
+ if (error instanceof Error) {
129
+ throw new Error(`Failed to search for models: ${error.message}`);
130
+ }
131
+ throw error;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Search for models with a specific filter (e.g., arxiv:XXXX.XXXXX)
137
+ */
138
+ async searchWithFilter(filter: string, limit: number = 10): Promise<string> {
139
+ try {
140
+ const apiParams: ModelApiParams = {
141
+ filter: filter,
142
+ limit: limit.toString(),
143
+ sort: 'downloads',
144
+ direction: '-1',
145
+ };
146
+
147
+ // Call the API
148
+ const models = await this.callApi<ModelApiResult[]>(apiParams);
149
+
150
+ if (models.length === 0) {
151
+ return `No models found referencing ${filter}.`;
152
+ }
153
+
154
+ return formatSearchResults(models, { limit });
155
+ } catch (error) {
156
+ if (error instanceof Error) {
157
+ throw new Error(`Failed to search for models: ${error.message}`);
158
+ }
159
+ throw error;
160
+ }
161
+ }
162
+ }
163
+
164
+ // Formatting Function
165
+ function formatSearchResults(models: ModelApiResult[], params: Partial<ModelSearchParams>): string {
166
+ const r: string[] = [];
167
+
168
+ // Build search description
169
+ const searchTerms = [];
170
+ if (params.query) searchTerms.push(`query "${params.query}"`);
171
+ if (params.author) searchTerms.push(`author "${params.author}"`);
172
+ if (params.task) searchTerms.push(`task "${params.task}"`);
173
+ if (params.library) searchTerms.push(`library "${params.library}"`);
174
+ if (params.sort) searchTerms.push(`sorted by ${params.sort} (descending)`);
175
+
176
+ const searchDesc = searchTerms.length > 0 ? ` matching ${searchTerms.join(', ')}` : '';
177
+
178
+ const resultText =
179
+ models.length === params.limit
180
+ ? `Showing first ${models.length.toString()} models${searchDesc}:`
181
+ : `Found ${models.length.toString()} models${searchDesc}:`;
182
+ r.push(resultText);
183
+ r.push('');
184
+
185
+ for (const model of models) {
186
+ r.push(`## ${model.id}`);
187
+ r.push('');
188
+
189
+ // Basic info line
190
+ const info = [];
191
+ if (model.pipeline_tag) info.push(`**Task:** ${model.pipeline_tag}`);
192
+ if (model.library_name) info.push(`**Library:** ${model.library_name}`);
193
+ if (model.downloads) info.push(`**Downloads:** ${formatNumber(model.downloads)}`);
194
+ if (model.likes) info.push(`**Likes:** ${model.likes.toString()}`);
195
+ if (model.trendingScore) info.push(`**Trending Score:** ${model.trendingScore.toString()}`);
196
+
197
+ if (info.length > 0) {
198
+ r.push(info.join(' | '));
199
+ r.push('');
200
+ }
201
+
202
+ // Tags
203
+ if (model.tags && model.tags.length > 0) {
204
+ r.push(`**Tags:** ${model.tags.slice(0, TAGS_TO_RETURN).join(', ')}`);
205
+ if (model.tags.length > TAGS_TO_RETURN) {
206
+ r.push(`*and ${(model.tags.length - TAGS_TO_RETURN).toString()} more...*`);
207
+ }
208
+ r.push('');
209
+ }
210
+
211
+ // Status indicators
212
+ const status = [];
213
+ if (model.private) status.push('🔐 Private');
214
+ if (status.length > 0) {
215
+ r.push(status.join(' | '));
216
+ r.push('');
217
+ }
218
+
219
+ // Dates
220
+ if (model.createdAt) {
221
+ r.push(`**Created:** ${formatDate(model.createdAt)}`);
222
+ }
223
+
224
+ r.push(`**Link:** [https://hf.co/${model.id}](https://hf.co/${model.id})`);
225
+ r.push('');
226
+ r.push('---');
227
+ r.push('');
228
+ }
229
+
230
+ return r.join('\n');
231
+ }