@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,263 @@
1
+ [
2
+ {
3
+ "author": "ginipick",
4
+ "authorData": {
5
+ "_id": "65acd60b57f263e3d0ff0647",
6
+ "avatarUrl": "https://cdn-avatars.huggingface.co/v1/production/uploads/65acd60b57f263e3d0ff0647/HENR9sR3CDchSDldrYOdS.png",
7
+ "fullname": "ginipick",
8
+ "name": "ginipick",
9
+ "type": "user",
10
+ "isPro": true,
11
+ "isHf": false,
12
+ "isHfAdmin": false,
13
+ "isMod": false,
14
+ "followerCount": 602
15
+ },
16
+ "colorFrom": "gray",
17
+ "colorTo": "pink",
18
+ "createdAt": "2024-08-22T02:55:22.000Z",
19
+ "emoji": "🦀🏆🦀",
20
+ "id": "ginipick/FLUXllama",
21
+ "lastModified": "2025-05-21T13:20:06.000Z",
22
+ "likes": 378,
23
+ "pinned": false,
24
+ "private": false,
25
+ "sdk": "gradio",
26
+ "repoType": "space",
27
+ "runtime": {
28
+ "stage": "RUNNING",
29
+ "hardware": {
30
+ "current": "zero-a10g",
31
+ "requested": "zero-a10g"
32
+ },
33
+ "storage": null,
34
+ "gcTimeout": 172800,
35
+ "replicas": {
36
+ "current": 1,
37
+ "requested": 1
38
+ },
39
+ "devMode": false,
40
+ "domains": [
41
+ {
42
+ "domain": "ginipick-fluxllama.hf.space",
43
+ "stage": "READY"
44
+ }
45
+ ],
46
+ "sha": "36a649401e539c622e2a397528ad02ce5f84fe20"
47
+ },
48
+ "shortDescription": "mcp_server & FLUX 4-bit Quantization(just 8GB VRAM)",
49
+ "title": "FLUXllama",
50
+ "isLikedByUser": false,
51
+ "originSpace": {
52
+ "name": "nyanko7/flux1-dev-nf4",
53
+ "author": {
54
+ "_id": "63467f5a7fb9f11870c411ee",
55
+ "avatarUrl": "https://cdn-avatars.huggingface.co/v1/production/uploads/63467f5a7fb9f11870c411ee/Ztk8IModHk8lDxZWnM1DJ.png",
56
+ "fullname": "Nyanko",
57
+ "name": "nyanko7",
58
+ "type": "user",
59
+ "isPro": false,
60
+ "isHf": false,
61
+ "isHfAdmin": false,
62
+ "isMod": false,
63
+ "followerCount": 206
64
+ }
65
+ },
66
+ "ai_short_description": "Generate images using text prompts",
67
+ "ai_category": "Image Generation",
68
+ "trendingScore": 8,
69
+ "semanticRelevancyScore": 0.9137691233545246
70
+ },
71
+ {
72
+ "author": "ginipick",
73
+ "authorData": {
74
+ "_id": "65acd60b57f263e3d0ff0647",
75
+ "avatarUrl": "https://cdn-avatars.huggingface.co/v1/production/uploads/65acd60b57f263e3d0ff0647/HENR9sR3CDchSDldrYOdS.png",
76
+ "fullname": "ginipick",
77
+ "name": "ginipick",
78
+ "type": "user",
79
+ "isPro": true,
80
+ "isHf": false,
81
+ "isHfAdmin": false,
82
+ "isMod": false,
83
+ "followerCount": 602
84
+ },
85
+ "colorFrom": "yellow",
86
+ "colorTo": "pink",
87
+ "createdAt": "2024-09-14T01:07:20.000Z",
88
+ "emoji": "💬⚡",
89
+ "id": "ginipick/Realtime-FLUX",
90
+ "lastModified": "2025-05-21T13:28:38.000Z",
91
+ "likes": 106,
92
+ "pinned": true,
93
+ "private": false,
94
+ "sdk": "gradio",
95
+ "repoType": "space",
96
+ "runtime": {
97
+ "stage": "RUNNING",
98
+ "hardware": {
99
+ "current": "zero-a10g",
100
+ "requested": "zero-a10g"
101
+ },
102
+ "storage": null,
103
+ "gcTimeout": 172800,
104
+ "replicas": {
105
+ "current": 1,
106
+ "requested": 1
107
+ },
108
+ "devMode": false,
109
+ "domains": [
110
+ {
111
+ "domain": "ginipick-realtime-flux.hf.space",
112
+ "stage": "READY"
113
+ }
114
+ ],
115
+ "sha": "6293da23835f10d329d91eb82fb32ba90f7b7b6d"
116
+ },
117
+ "shortDescription": "mcp_server & High quality Images in Realtime",
118
+ "title": "Realtime FLUX Image",
119
+ "isLikedByUser": false,
120
+ "originSpace": {
121
+ "name": "KingNish/Realtime-FLUX",
122
+ "author": {
123
+ "_id": "6612aedf09f16e7347dfa7e1",
124
+ "avatarUrl": "https://cdn-avatars.huggingface.co/v1/production/uploads/6612aedf09f16e7347dfa7e1/bPYjBXCedY_1fSIPjoBTY.jpeg",
125
+ "fullname": "Nishith Jain",
126
+ "name": "KingNish",
127
+ "type": "user",
128
+ "isPro": false,
129
+ "isHf": false,
130
+ "isHfAdmin": false,
131
+ "isMod": false,
132
+ "followerCount": 1206
133
+ }
134
+ },
135
+ "ai_short_description": "Generate images from text prompts",
136
+ "ai_category": "Image Generation",
137
+ "trendingScore": 3.5,
138
+ "semanticRelevancyScore": 0.6803295182592884
139
+ },
140
+ {
141
+ "author": "evalstate",
142
+ "authorData": {
143
+ "_id": "6319b36409baf858241f0f89",
144
+ "avatarUrl": "/avatars/909635453bf62a2a7118a01dd51b811c.svg",
145
+ "fullname": "shaun smith",
146
+ "name": "evalstate",
147
+ "type": "user",
148
+ "isPro": true,
149
+ "isHf": false,
150
+ "isHfAdmin": false,
151
+ "isMod": false,
152
+ "followerCount": 3
153
+ },
154
+ "colorFrom": "yellow",
155
+ "colorTo": "pink",
156
+ "createdAt": "2024-12-08T17:01:13.000Z",
157
+ "emoji": "🏎️💨",
158
+ "id": "evalstate/flux1_schnell",
159
+ "lastModified": "2025-04-30T18:45:28.000Z",
160
+ "likes": 1,
161
+ "pinned": false,
162
+ "private": false,
163
+ "sdk": "gradio",
164
+ "repoType": "space",
165
+ "runtime": {
166
+ "stage": "RUNNING",
167
+ "hardware": {
168
+ "current": "zero-a10g",
169
+ "requested": "zero-a10g"
170
+ },
171
+ "storage": null,
172
+ "gcTimeout": 172800,
173
+ "replicas": {
174
+ "current": 1,
175
+ "requested": 1
176
+ },
177
+ "devMode": false,
178
+ "domains": [
179
+ {
180
+ "domain": "evalstate-flux-1-schnell.hf.space",
181
+ "stage": "READY"
182
+ },
183
+ {
184
+ "domain": "evalstate-flux1-schnell.hf.space",
185
+ "stage": "READY"
186
+ }
187
+ ],
188
+ "sha": "ae85ed512e3324ffbabb8c1e2a2e490fd5e1e252"
189
+ },
190
+ "title": "FLUX.1 [Schnell]",
191
+ "isLikedByUser": false,
192
+ "originSpace": {
193
+ "name": "black-forest-labs/FLUX.1-schnell",
194
+ "author": {
195
+ "avatarUrl": "https://cdn-avatars.huggingface.co/v1/production/uploads/62cfefa74b3e8dc1e32e38bf/GgkglHn3sIo6C5XGTtZSs.png",
196
+ "fullname": "Black Forest Labs",
197
+ "name": "black-forest-labs",
198
+ "type": "org",
199
+ "isHf": false,
200
+ "isHfAdmin": false,
201
+ "isMod": false,
202
+ "isEnterprise": false,
203
+ "followerCount": 18355
204
+ }
205
+ },
206
+ "ai_short_description": "Generate images from text prompts",
207
+ "ai_category": "Image Generation",
208
+ "trendingScore": 0,
209
+ "semanticRelevancyScore": 0.25
210
+ },
211
+ {
212
+ "author": "black-forest-labs",
213
+ "authorData": {
214
+ "avatarUrl": "https://cdn-avatars.huggingface.co/v1/production/uploads/62cfefa74b3e8dc1e32e38bf/GgkglHn3sIo6C5XGTtZSs.png",
215
+ "fullname": "Black Forest Labs",
216
+ "name": "black-forest-labs",
217
+ "type": "org",
218
+ "isHf": false,
219
+ "isHfAdmin": false,
220
+ "isMod": false,
221
+ "isEnterprise": false,
222
+ "followerCount": 18356
223
+ },
224
+ "colorFrom": "yellow",
225
+ "colorTo": "pink",
226
+ "createdAt": "2024-08-01T09:32:41.000Z",
227
+ "emoji": "🏎️💨",
228
+ "id": "black-forest-labs/FLUX.1-schnell",
229
+ "lastModified": "2024-08-09T14:51:04.000Z",
230
+ "likes": 4804,
231
+ "pinned": false,
232
+ "private": false,
233
+ "sdk": "gradio",
234
+ "repoType": "space",
235
+ "runtime": {
236
+ "stage": "RUNNING",
237
+ "hardware": {
238
+ "current": "zero-a10g",
239
+ "requested": "zero-a10g"
240
+ },
241
+ "storage": null,
242
+ "gcTimeout": 172800,
243
+ "replicas": {
244
+ "current": 1,
245
+ "requested": "auto"
246
+ },
247
+ "devMode": false,
248
+ "domains": [
249
+ {
250
+ "domain": "black-forest-labs-flux-1-schnell.hf.space",
251
+ "stage": "READY"
252
+ }
253
+ ],
254
+ "sha": "86f96a46582e9bbbc6245e4b1eb7d72cd8ec7bda"
255
+ },
256
+ "title": "FLUX.1 [Schnell]",
257
+ "isLikedByUser": false,
258
+ "ai_short_description": "Generate images from text prompts",
259
+ "ai_category": "Image Generation",
260
+ "trendingScore": 24,
261
+ "semanticRelevancyScore": 0.6919353880044354
262
+ }
263
+ ]
@@ -0,0 +1,57 @@
1
+ import { describe, beforeEach, afterEach, it, expect } from 'vitest';
2
+ import type { PaperSearchResult } from '../src/paper-search.js';
3
+ import { authors } from '../src/paper-search.js';
4
+ import { published } from '../src/paper-search.js';
5
+ import { readFileSync } from 'fs';
6
+ import path from 'path';
7
+ import { formatDate } from '../src/utilities.js';
8
+
9
+ describe('PaperSearchService', () => {
10
+ let kazakh: PaperSearchResult[];
11
+ function loadTestData(filename: string) {
12
+ const filePath = path.join(__dirname, '../test/fixtures', filename);
13
+ const fileContent = readFileSync(filePath, 'utf-8');
14
+ return JSON.parse(fileContent) as PaperSearchResult[];
15
+ }
16
+
17
+ beforeEach(() => {
18
+ kazakh = loadTestData('paper_result_kazakh.json');
19
+ });
20
+
21
+ afterEach(() => {});
22
+
23
+ it('format the published on date correctly', () => {
24
+ expect(published(kazakh[0]?.paper.publishedAt)).toBe('Published on 6 Apr, 2024');
25
+ });
26
+
27
+ //github.com/huggingface/huggingface_hub/blob/a26b93e8ba0b51ce76ce5c2044896587c47c6b60/src/huggingface_hub/utils/_datetime.py#L50-L62
28
+ it('handles times with no decimal point', () => {
29
+ expect(published('2021-08-30T00:04:01Z')).toBe('Published on 30 Aug, 2021');
30
+ });
31
+
32
+ it('deals with bad inputs', () => {
33
+ expect(published('invalid_date')).toBe('Publication date not available');
34
+ });
35
+
36
+ it('shows us not available for empty list', () => {
37
+ expect(authors([])).toBe('**Authors:** Not available');
38
+ });
39
+
40
+ it('shows us hf profiles for authors when known ', () => {
41
+ expect(authors(kazakh[2]?.paper?.authors || [])).toBe(
42
+ '**Authors:** Rustem Yeshpanov ([yeshpanovrustem](https://hf.co/yeshpanovrustem)), Huseyin Atakan Varol'
43
+ );
44
+ });
45
+
46
+ it('shows us hf profiles when known ', () => {
47
+ expect(authors(kazakh[2]?.paper?.authors || [])).toBe(
48
+ '**Authors:** Rustem Yeshpanov ([yeshpanovrustem](https://hf.co/yeshpanovrustem)), Huseyin Atakan Varol'
49
+ );
50
+ });
51
+
52
+ it("it won't break with herd of llamas that has 500 authors", () => {
53
+ expect(authors(kazakh[0]?.paper?.authors || [], 1)).toBe(
54
+ '**Authors:** Rustem Yeshpanov ([yeshpanovrustem](https://hf.co/yeshpanovrustem)), and 4 more.'
55
+ );
56
+ });
57
+ });
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { extractArxivIdFromInput } from '../src/paper-summary.js';
3
+
4
+ describe('extractArxivIdFromInput', () => {
5
+ it('should handle plain arXiv ID format', () => {
6
+ expect(extractArxivIdFromInput('2502.16161')).toBe('2502.16161');
7
+ expect(extractArxivIdFromInput('2301.12345')).toBe('2301.12345');
8
+ expect(extractArxivIdFromInput('1907.11692')).toBe('1907.11692');
9
+ });
10
+
11
+ it('should handle arxiv: prefix', () => {
12
+ expect(extractArxivIdFromInput('arxiv:2502.16161')).toBe('2502.16161');
13
+ expect(extractArxivIdFromInput('ARXIV:2502.16161')).toBe('2502.16161');
14
+ expect(extractArxivIdFromInput('ArXiv:2502.16161')).toBe('2502.16161');
15
+ });
16
+
17
+ it('should handle arxiv. prefix (typo)', () => {
18
+ expect(extractArxivIdFromInput('arxiv.2502.16161')).toBe('2502.16161');
19
+ expect(extractArxivIdFromInput('ARXIV.2502.16161')).toBe('2502.16161');
20
+ });
21
+
22
+ it('should handle Hugging Face paper URLs', () => {
23
+ expect(extractArxivIdFromInput('https://huggingface.co/papers/2502.16161')).toBe('2502.16161');
24
+ expect(extractArxivIdFromInput('https://hf.co/papers/2502.16161')).toBe('2502.16161');
25
+ expect(extractArxivIdFromInput('huggingface.co/papers/2502.16161')).toBe('2502.16161');
26
+ expect(extractArxivIdFromInput('hf.co/papers/2502.16161')).toBe('2502.16161');
27
+ });
28
+
29
+ it('should handle arXiv.org URLs', () => {
30
+ expect(extractArxivIdFromInput('https://arxiv.org/abs/2502.16161')).toBe('2502.16161');
31
+ expect(extractArxivIdFromInput('arxiv.org/abs/2502.16161')).toBe('2502.16161');
32
+ expect(extractArxivIdFromInput('http://www.arxiv.org/abs/2502.16161')).toBe('2502.16161');
33
+ });
34
+
35
+ it('should trim whitespace', () => {
36
+ expect(extractArxivIdFromInput(' 2502.16161 ')).toBe('2502.16161');
37
+ expect(extractArxivIdFromInput('\tarxiv:2502.16161\n')).toBe('2502.16161');
38
+ });
39
+
40
+ it('should reject invalid formats', () => {
41
+ // Invalid arXiv ID patterns
42
+ expect(() => extractArxivIdFromInput('2502.161')).toThrow('Invalid arXiv ID format');
43
+ expect(() => extractArxivIdFromInput('502.16161')).toThrow('Invalid arXiv ID format');
44
+ expect(() => extractArxivIdFromInput('2502.1616161')).toThrow('Invalid arXiv ID format');
45
+ expect(() => extractArxivIdFromInput('abcd.efgh')).toThrow(
46
+ 'URL must be from arxiv.org, huggingface.co, or hf.co. Got: abcd.efgh'
47
+ );
48
+
49
+ // Invalid prefixes
50
+ expect(() => extractArxivIdFromInput('arxiv:2502.161')).toThrow('Invalid arXiv ID format after "arxiv:" prefix');
51
+ expect(() => extractArxivIdFromInput('arxiv.abcd.efgh')).toThrow('Invalid arXiv ID format after "arxiv." prefix');
52
+
53
+ // Invalid domains
54
+ expect(() => extractArxivIdFromInput('https://example.com/papers/2502.16161')).toThrow(
55
+ 'URL must be from arxiv.org, huggingface.co, or hf.co. Got: example.com'
56
+ );
57
+ expect(() => extractArxivIdFromInput('github.com/papers/2502.16161')).toThrow(
58
+ 'URL must be from arxiv.org, huggingface.co, or hf.co. Got: github.com'
59
+ );
60
+
61
+ // Invalid paths on valid domains
62
+ expect(() => extractArxivIdFromInput('https://arxiv.org/pdf/2502.16161')).toThrow(
63
+ 'arXiv URL must be in format: arxiv.org/abs/YYMM.NNNNN'
64
+ );
65
+ expect(() => extractArxivIdFromInput('https://hf.co/models/2502.16161')).toThrow(
66
+ 'Hugging Face URL must be in format: hf.co/papers/YYMM.NNNNN'
67
+ );
68
+
69
+ // Too short
70
+ expect(() => extractArxivIdFromInput('25')).toThrow('Invalid arXiv ID format');
71
+ expect(() => extractArxivIdFromInput('')).toThrow('Paper ID is required');
72
+
73
+ // With query params or fragments
74
+ expect(() => extractArxivIdFromInput('hf.co/papers/2502.16161?foo=bar')).toThrow(
75
+ 'URL must contain only the paper ID path'
76
+ );
77
+ expect(() => extractArxivIdFromInput('hf.co/papers/2502.16161#section')).toThrow(
78
+ 'URL must contain only the paper ID path'
79
+ );
80
+ });
81
+
82
+ it('should reject obvious domain names without path', () => {
83
+ expect(() => extractArxivIdFromInput('hf.co')).toThrow(
84
+ 'Hugging Face URL must be in format: hf.co/papers/YYMM.NNNNN'
85
+ );
86
+ expect(() => extractArxivIdFromInput('huggingface.com')).toThrow(
87
+ 'URL must be from arxiv.org, huggingface.co, or hf.co. Got: huggingface.com'
88
+ );
89
+ expect(() => extractArxivIdFromInput('arxiv.org')).toThrow('arXiv URL must be in format: arxiv.org/abs/YYMM.NNNNN');
90
+ });
91
+
92
+ it('should handle edge cases for domain validation', () => {
93
+ // Valid domains with valid paths
94
+ expect(extractArxivIdFromInput('http://arxiv.org/abs/2301.12345')).toBe('2301.12345');
95
+ expect(extractArxivIdFromInput('https://www.arxiv.org/abs/2301.12345')).toBe('2301.12345');
96
+
97
+ // Invalid domains that might look similar
98
+ expect(() => extractArxivIdFromInput('arxiv.com/abs/2502.16161')).toThrow(
99
+ 'URL must be from arxiv.org, huggingface.co, or hf.co. Got: arxiv.com'
100
+ );
101
+ expect(() => extractArxivIdFromInput('huggingface.ai/papers/2502.16161')).toThrow(
102
+ 'URL must be from arxiv.org, huggingface.co, or hf.co. Got: huggingface.ai'
103
+ );
104
+ expect(() => extractArxivIdFromInput('hf.ai/papers/2502.16161')).toThrow(
105
+ 'URL must be from arxiv.org, huggingface.co, or hf.co. Got: hf.ai'
106
+ );
107
+
108
+ // URLs without protocol should also work
109
+ expect(extractArxivIdFromInput('arxiv.org/abs/2301.12345')).toBe('2301.12345');
110
+ expect(extractArxivIdFromInput('hf.co/papers/2301.12345')).toBe('2301.12345');
111
+ expect(extractArxivIdFromInput('huggingface.co/papers/2301.12345')).toBe('2301.12345');
112
+ });
113
+ });
@@ -0,0 +1,232 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { SpaceFilesTool, SPACE_FILES_TOOL_CONFIG } from '../src/space-files.js';
3
+ import * as hub from '@huggingface/hub';
4
+
5
+ // Mock the @huggingface/hub module
6
+ vi.mock('@huggingface/hub', () => ({
7
+ spaceInfo: vi.fn(),
8
+ listFiles: vi.fn(),
9
+ }));
10
+
11
+ describe('SpaceFilesTool', () => {
12
+ let tool: SpaceFilesTool;
13
+
14
+ beforeEach(() => {
15
+ tool = new SpaceFilesTool(undefined, 'testuser');
16
+ vi.clearAllMocks();
17
+ });
18
+
19
+ describe('getSpaceFilesWithUrls', () => {
20
+ it('should list files for a static space with subdomain', async () => {
21
+ // Mock space info response
22
+ vi.mocked(hub.spaceInfo).mockResolvedValue({
23
+ id: 'evalstate/filedrop',
24
+ sdk: 'static',
25
+ subdomain: 'evalstate-filedrop',
26
+ } as any);
27
+
28
+ // Mock file listing
29
+ const mockFiles = [
30
+ {
31
+ type: 'file',
32
+ path: 'index.html',
33
+ size: 1024,
34
+ lastCommit: { date: '2024-01-01T00:00:00Z' },
35
+ lfs: null,
36
+ },
37
+ {
38
+ type: 'file',
39
+ path: 'css/style.css',
40
+ size: 2048,
41
+ lastCommit: { date: '2024-01-02T00:00:00Z' },
42
+ lfs: null,
43
+ },
44
+ ];
45
+
46
+ // Create async generator for listFiles
47
+ async function* mockListFiles() {
48
+ for (const file of mockFiles) {
49
+ yield file;
50
+ }
51
+ }
52
+
53
+ vi.mocked(hub.listFiles).mockReturnValue(mockListFiles() as any);
54
+
55
+ const files = await tool.getSpaceFilesWithUrls('evalstate/filedrop');
56
+
57
+ expect(files).toHaveLength(2);
58
+ expect(files[0]).toMatchObject({
59
+ path: 'css/style.css',
60
+ size: 2048,
61
+ type: 'file',
62
+ url: 'https://huggingface.co/spaces/evalstate/filedrop/resolve/main/css/style.css',
63
+ sizeFormatted: '2.0 KB',
64
+ lfs: false,
65
+ });
66
+ expect(files[1]).toMatchObject({
67
+ path: 'index.html',
68
+ size: 1024,
69
+ type: 'file',
70
+ url: 'https://huggingface.co/spaces/evalstate/filedrop/resolve/main/index.html',
71
+ sizeFormatted: '1.0 KB',
72
+ lfs: false,
73
+ });
74
+ });
75
+
76
+ it('should handle spaces without subdomain', async () => {
77
+ // Mock space info response without subdomain
78
+ vi.mocked(hub.spaceInfo).mockResolvedValue({
79
+ id: 'test/space',
80
+ sdk: 'static',
81
+ } as any);
82
+
83
+ // Mock file listing
84
+ async function* mockListFiles() {
85
+ yield {
86
+ type: 'file',
87
+ path: 'index.html',
88
+ size: 512,
89
+ lastCommit: null,
90
+ lfs: null,
91
+ };
92
+ }
93
+
94
+ vi.mocked(hub.listFiles).mockReturnValue(mockListFiles() as any);
95
+
96
+ const files = await tool.getSpaceFilesWithUrls('test/space');
97
+
98
+ expect(files).toHaveLength(1);
99
+ expect(files[0].url).toBe('https://huggingface.co/spaces/test/space/resolve/main/index.html');
100
+ });
101
+
102
+ it('should throw error for non-static spaces', async () => {
103
+ // Mock space info response with non-static SDK
104
+ vi.mocked(hub.spaceInfo).mockResolvedValue({
105
+ id: 'test/gradio-app',
106
+ sdk: 'gradio',
107
+ } as any);
108
+
109
+ await expect(tool.getSpaceFilesWithUrls('test/gradio-app')).rejects.toThrow(
110
+ 'Space "test/gradio-app" is not a static space (found: gradio). This tool only works with static spaces.'
111
+ );
112
+ });
113
+ });
114
+
115
+ describe('listFiles', () => {
116
+ beforeEach(() => {
117
+ // Mock a successful static space
118
+ vi.mocked(hub.spaceInfo).mockResolvedValue({
119
+ id: 'evalstate/filedrop',
120
+ sdk: 'static',
121
+ subdomain: 'evalstate-filedrop',
122
+ } as any);
123
+
124
+ // Mock file listing
125
+ async function* mockListFiles() {
126
+ yield {
127
+ type: 'file',
128
+ path: 'index.html',
129
+ size: 1024,
130
+ lastCommit: { date: '2024-01-01T00:00:00Z' },
131
+ lfs: null,
132
+ };
133
+ yield {
134
+ type: 'file',
135
+ path: 'js/app.js',
136
+ size: 4096,
137
+ lastCommit: { date: '2024-01-02T00:00:00Z' },
138
+ lfs: null,
139
+ };
140
+ }
141
+
142
+ vi.mocked(hub.listFiles).mockReturnValue(mockListFiles() as any);
143
+ });
144
+
145
+ it('should generate detailed markdown', async () => {
146
+ const result = await tool.listFiles({ spaceName: 'evalstate/filedrop' });
147
+
148
+ expect(result).toContain('# Files in Space: evalstate/filedrop');
149
+ expect(result).toContain('**Total Files**: 2');
150
+ expect(result).toContain('**Total Size**: 5.1 KB');
151
+ expect(result).toContain('| File Path | Size | Type | Last Modified | URL |');
152
+ expect(result).toContain('🌐 index.html');
153
+ expect(result).toContain('📜 app.js');
154
+ expect(result).toContain('## Direct Access Examples');
155
+ });
156
+
157
+ it('should filter by file type when specified', async () => {
158
+ const result = await tool.listFiles({ spaceName: 'evalstate/filedrop', fileType: 'all' });
159
+
160
+ expect(result).toContain('# Files in Space: evalstate/filedrop');
161
+ expect(result).toContain('| File Path | Size | Type | Last Modified | URL |');
162
+ expect(result).toContain('🌐 index.html');
163
+ expect(result).toContain('📜 app.js');
164
+ expect(result).toContain('## Direct Access Examples');
165
+ });
166
+ });
167
+
168
+ describe('SPACE_FILES_TOOL_CONFIG', () => {
169
+ it('should have correct base configuration', () => {
170
+ expect(SPACE_FILES_TOOL_CONFIG.name).toBe('space_files');
171
+ expect(SPACE_FILES_TOOL_CONFIG.annotations.readOnlyHint).toBe(true);
172
+ expect(SPACE_FILES_TOOL_CONFIG.annotations.destructiveHint).toBe(false);
173
+ });
174
+
175
+ it('should validate schema correctly', () => {
176
+ const schema = SPACE_FILES_TOOL_CONFIG.schema;
177
+
178
+ // Test default values
179
+ const defaultResult = schema.parse({});
180
+ expect(defaultResult).toEqual({
181
+ fileType: 'all',
182
+ });
183
+
184
+ // Test custom values
185
+ const customResult = schema.parse({
186
+ spaceName: 'user/space',
187
+ fileType: 'image',
188
+ });
189
+ expect(customResult).toEqual({
190
+ spaceName: 'user/space',
191
+ fileType: 'image',
192
+ });
193
+
194
+ // Test invalid fileType
195
+ expect(() => schema.parse({ fileType: 'invalid' })).toThrow();
196
+ });
197
+ });
198
+
199
+ describe('createToolConfig', () => {
200
+ it('should create config with username', () => {
201
+ const config = SpaceFilesTool.createToolConfig('testuser');
202
+ expect(config.name).toBe('space_files');
203
+ expect(config.description).toContain('testuser/filedrop');
204
+ });
205
+ });
206
+
207
+ describe('default spaceName behavior', () => {
208
+ it('should use username/filedrop as default when no spaceName provided', async () => {
209
+ // Mock space info response
210
+ vi.mocked(hub.spaceInfo).mockResolvedValue({
211
+ id: 'testuser/filedrop',
212
+ sdk: 'static',
213
+ subdomain: 'testuser-filedrop',
214
+ } as any);
215
+
216
+ // Mock empty file listing
217
+ async function* mockListFiles() {
218
+ // Empty generator
219
+ }
220
+ vi.mocked(hub.listFiles).mockReturnValue(mockListFiles() as any);
221
+
222
+ const result = await tool.listFiles({});
223
+
224
+ expect(result).toContain('# Files in Space: testuser/filedrop');
225
+ expect(vi.mocked(hub.spaceInfo)).toHaveBeenCalledWith(
226
+ expect.objectContaining({
227
+ name: 'testuser/filedrop',
228
+ })
229
+ );
230
+ });
231
+ });
232
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, beforeEach, afterEach, it, expect } from 'vitest';
2
+ import { readFileSync } from 'fs';
3
+ import path from 'path';
4
+ import { SpaceSearchResult } from '../dist/space-search.js';
5
+
6
+ describe('SpaceSearchService', () => {
7
+ let space: SpaceSearchResult[];
8
+ function loadTestData(filename: string) {
9
+ const filePath = path.join(__dirname, '../test/fixtures', filename);
10
+ const fileContent = readFileSync(filePath, 'utf-8');
11
+ return JSON.parse(fileContent) as SpaceSearchResult[];
12
+ }
13
+
14
+ beforeEach(() => {
15
+ space = loadTestData('space-result.json');
16
+ });
17
+
18
+ afterEach(() => {});
19
+
20
+ it('read the test file', () => {
21
+ expect('evalstate').toBe(space[2].author);
22
+ });
23
+
24
+ it('picked up other results', () => {
25
+ expect('RUNNING').toBe(space[2].runtime.stage);
26
+ expect('Image Generation').toBe(space[2].ai_category);
27
+ expect('Generate images from text prompts').toBe(space[2].ai_short_description);
28
+ });
29
+ });