@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.
- package/LICENSE +21 -0
- package/dist/dataset-detail.d.ts +26 -0
- package/dist/dataset-detail.d.ts.map +1 -0
- package/dist/dataset-detail.js +157 -0
- package/dist/dataset-detail.js.map +1 -0
- package/dist/dataset-search.d.ts +62 -0
- package/dist/dataset-search.d.ts.map +1 -0
- package/dist/dataset-search.js +158 -0
- package/dist/dataset-search.js.map +1 -0
- package/dist/duplicate-space.d.ts +75 -0
- package/dist/duplicate-space.d.ts.map +1 -0
- package/dist/duplicate-space.js +189 -0
- package/dist/duplicate-space.js.map +1 -0
- package/dist/error-messages.d.ts +4 -0
- package/dist/error-messages.d.ts.map +1 -0
- package/dist/error-messages.js +30 -0
- package/dist/error-messages.js.map +1 -0
- package/dist/hf-api-call.d.ts +18 -0
- package/dist/hf-api-call.d.ts.map +1 -0
- package/dist/hf-api-call.js +105 -0
- package/dist/hf-api-call.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/model-detail.d.ts +26 -0
- package/dist/model-detail.d.ts.map +1 -0
- package/dist/model-detail.js +224 -0
- package/dist/model-detail.js.map +1 -0
- package/dist/model-search.d.ts +64 -0
- package/dist/model-search.d.ts.map +1 -0
- package/dist/model-search.js +161 -0
- package/dist/model-search.js.map +1 -0
- package/dist/paper-search.d.ts +58 -0
- package/dist/paper-search.d.ts.map +1 -0
- package/dist/paper-search.js +114 -0
- package/dist/paper-search.js.map +1 -0
- package/dist/paper-summary.d.ts +35 -0
- package/dist/paper-summary.d.ts.map +1 -0
- package/dist/paper-summary.js +187 -0
- package/dist/paper-summary.js.map +1 -0
- package/dist/space-files.d.ts +44 -0
- package/dist/space-files.d.ts.map +1 -0
- package/dist/space-files.js +242 -0
- package/dist/space-files.js.map +1 -0
- package/dist/space-info.d.ts +56 -0
- package/dist/space-info.d.ts.map +1 -0
- package/dist/space-info.js +135 -0
- package/dist/space-info.js.map +1 -0
- package/dist/space-search.d.ts +71 -0
- package/dist/space-search.d.ts.map +1 -0
- package/dist/space-search.js +95 -0
- package/dist/space-search.js.map +1 -0
- package/dist/tool-ids.d.ts +23 -0
- package/dist/tool-ids.d.ts.map +1 -0
- package/dist/tool-ids.js +55 -0
- package/dist/tool-ids.js.map +1 -0
- package/dist/user-summary.d.ts +56 -0
- package/dist/user-summary.d.ts.map +1 -0
- package/dist/user-summary.js +271 -0
- package/dist/user-summary.js.map +1 -0
- package/dist/utilities.d.ts +8 -0
- package/dist/utilities.d.ts.map +1 -0
- package/dist/utilities.js +53 -0
- package/dist/utilities.js.map +1 -0
- package/eslint.config.js +43 -0
- package/package.json +47 -0
- package/src/dataset-detail.ts +257 -0
- package/src/dataset-search.ts +237 -0
- package/src/duplicate-space.ts +263 -0
- package/src/error-messages.ts +57 -0
- package/src/hf-api-call.ts +182 -0
- package/src/index.ts +18 -0
- package/src/model-detail.ts +359 -0
- package/src/model-search.ts +231 -0
- package/src/paper-search.ts +188 -0
- package/src/paper-summary.ts +303 -0
- package/src/space-files.ts +325 -0
- package/src/space-info.ts +190 -0
- package/src/space-search.ts +177 -0
- package/src/tool-ids.ts +84 -0
- package/src/user-summary.ts +421 -0
- package/src/utilities.ts +64 -0
- package/test/duplicate-space.spec.ts +41 -0
- package/test/fixtures/paper_result_kazakh.json +854 -0
- package/test/fixtures/space-result.json +263 -0
- package/test/paper-search.spec.ts +57 -0
- package/test/paper-summary.spec.ts +113 -0
- package/test/space-files.spec.ts +232 -0
- package/test/space-search.spec.ts +29 -0
- package/test/user-summary.spec.ts +131 -0
- package/tsconfig.json +31 -0
- package/vitest.config.ts +11 -0
package/src/tool-ids.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical tool IDs exported from their respective tool configurations
|
|
3
|
+
* This ensures single source of truth for all tool identifiers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
SEMANTIC_SEARCH_TOOL_CONFIG,
|
|
8
|
+
MODEL_SEARCH_TOOL_CONFIG,
|
|
9
|
+
MODEL_DETAIL_TOOL_CONFIG,
|
|
10
|
+
PAPER_SEARCH_TOOL_CONFIG,
|
|
11
|
+
DATASET_SEARCH_TOOL_CONFIG,
|
|
12
|
+
DATASET_DETAIL_TOOL_CONFIG,
|
|
13
|
+
DUPLICATE_SPACE_TOOL_CONFIG,
|
|
14
|
+
SPACE_INFO_TOOL_CONFIG,
|
|
15
|
+
SPACE_FILES_TOOL_CONFIG,
|
|
16
|
+
USER_SUMMARY_PROMPT_CONFIG,
|
|
17
|
+
PAPER_SUMMARY_PROMPT_CONFIG,
|
|
18
|
+
} from './index.js';
|
|
19
|
+
|
|
20
|
+
// Extract tool IDs from their configs (single source of truth)
|
|
21
|
+
export const SPACE_SEARCH_TOOL_ID = SEMANTIC_SEARCH_TOOL_CONFIG.name;
|
|
22
|
+
export const MODEL_SEARCH_TOOL_ID = MODEL_SEARCH_TOOL_CONFIG.name;
|
|
23
|
+
export const MODEL_DETAIL_TOOL_ID = MODEL_DETAIL_TOOL_CONFIG.name;
|
|
24
|
+
export const PAPER_SEARCH_TOOL_ID = PAPER_SEARCH_TOOL_CONFIG.name;
|
|
25
|
+
export const DATASET_SEARCH_TOOL_ID = DATASET_SEARCH_TOOL_CONFIG.name;
|
|
26
|
+
export const DATASET_DETAIL_TOOL_ID = DATASET_DETAIL_TOOL_CONFIG.name;
|
|
27
|
+
export const DUPLICATE_SPACE_TOOL_ID = DUPLICATE_SPACE_TOOL_CONFIG.name;
|
|
28
|
+
export const SPACE_INFO_TOOL_ID = SPACE_INFO_TOOL_CONFIG.name;
|
|
29
|
+
export const SPACE_FILES_TOOL_ID = SPACE_FILES_TOOL_CONFIG.name;
|
|
30
|
+
export const USER_SUMMARY_PROMPT_ID = USER_SUMMARY_PROMPT_CONFIG.name;
|
|
31
|
+
export const PAPER_SUMMARY_PROMPT_ID = PAPER_SUMMARY_PROMPT_CONFIG.name;
|
|
32
|
+
|
|
33
|
+
// Complete list of all built-in tool IDs
|
|
34
|
+
export const ALL_BUILTIN_TOOL_IDS = [
|
|
35
|
+
SPACE_SEARCH_TOOL_ID,
|
|
36
|
+
MODEL_SEARCH_TOOL_ID,
|
|
37
|
+
MODEL_DETAIL_TOOL_ID,
|
|
38
|
+
PAPER_SEARCH_TOOL_ID,
|
|
39
|
+
DATASET_SEARCH_TOOL_ID,
|
|
40
|
+
DATASET_DETAIL_TOOL_ID,
|
|
41
|
+
DUPLICATE_SPACE_TOOL_ID,
|
|
42
|
+
SPACE_INFO_TOOL_ID,
|
|
43
|
+
SPACE_FILES_TOOL_ID,
|
|
44
|
+
] as const;
|
|
45
|
+
// Grouped tool IDs for bouquet configurations
|
|
46
|
+
export const TOOL_ID_GROUPS = {
|
|
47
|
+
search: [SPACE_SEARCH_TOOL_ID, MODEL_SEARCH_TOOL_ID, DATASET_SEARCH_TOOL_ID, PAPER_SEARCH_TOOL_ID] as const,
|
|
48
|
+
spaces: [SPACE_SEARCH_TOOL_ID, DUPLICATE_SPACE_TOOL_ID, SPACE_INFO_TOOL_ID, SPACE_FILES_TOOL_ID] as const,
|
|
49
|
+
detail: [MODEL_DETAIL_TOOL_ID, DATASET_DETAIL_TOOL_ID] as const,
|
|
50
|
+
hf_api: [
|
|
51
|
+
SPACE_SEARCH_TOOL_ID,
|
|
52
|
+
MODEL_SEARCH_TOOL_ID,
|
|
53
|
+
DATASET_SEARCH_TOOL_ID,
|
|
54
|
+
PAPER_SEARCH_TOOL_ID,
|
|
55
|
+
MODEL_DETAIL_TOOL_ID,
|
|
56
|
+
DATASET_DETAIL_TOOL_ID,
|
|
57
|
+
] as const,
|
|
58
|
+
all: [...ALL_BUILTIN_TOOL_IDS] as const,
|
|
59
|
+
} as const;
|
|
60
|
+
|
|
61
|
+
// TypeScript type for built-in tool IDs
|
|
62
|
+
export type BuiltinToolId = (typeof ALL_BUILTIN_TOOL_IDS)[number];
|
|
63
|
+
|
|
64
|
+
// Type guard function
|
|
65
|
+
export function isValidBuiltinToolId(toolId: string): toolId is BuiltinToolId {
|
|
66
|
+
return (ALL_BUILTIN_TOOL_IDS as readonly string[]).includes(toolId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Helper to get tool description from configs
|
|
70
|
+
export function getToolDescription(toolId: BuiltinToolId): string {
|
|
71
|
+
const configs = {
|
|
72
|
+
[SPACE_SEARCH_TOOL_ID]: SEMANTIC_SEARCH_TOOL_CONFIG,
|
|
73
|
+
[MODEL_SEARCH_TOOL_ID]: MODEL_SEARCH_TOOL_CONFIG,
|
|
74
|
+
[MODEL_DETAIL_TOOL_ID]: MODEL_DETAIL_TOOL_CONFIG,
|
|
75
|
+
[PAPER_SEARCH_TOOL_ID]: PAPER_SEARCH_TOOL_CONFIG,
|
|
76
|
+
[DATASET_SEARCH_TOOL_ID]: DATASET_SEARCH_TOOL_CONFIG,
|
|
77
|
+
[DATASET_DETAIL_TOOL_ID]: DATASET_DETAIL_TOOL_CONFIG,
|
|
78
|
+
[DUPLICATE_SPACE_TOOL_ID]: DUPLICATE_SPACE_TOOL_CONFIG,
|
|
79
|
+
[SPACE_INFO_TOOL_ID]: SPACE_INFO_TOOL_CONFIG,
|
|
80
|
+
[SPACE_FILES_TOOL_ID]: SPACE_FILES_TOOL_CONFIG,
|
|
81
|
+
} as const;
|
|
82
|
+
|
|
83
|
+
return configs[toolId]?.description || `Tool: ${toolId}`;
|
|
84
|
+
}
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { HfApiCall, HfApiError } from './hf-api-call.js';
|
|
3
|
+
import { formatDate, formatNumber } from './utilities.js';
|
|
4
|
+
import { ModelSearchTool } from './model-search.js';
|
|
5
|
+
import { DatasetSearchTool } from './dataset-search.js';
|
|
6
|
+
import { SpaceSearchTool } from './space-search.js';
|
|
7
|
+
import { PaperSearchTool } from './paper-search.js';
|
|
8
|
+
|
|
9
|
+
// User Summary Prompt Configuration
|
|
10
|
+
export const USER_SUMMARY_PROMPT_CONFIG = {
|
|
11
|
+
name: 'User Summary',
|
|
12
|
+
description:
|
|
13
|
+
'Generate a summary of a Hugging Face user including their profile, models, datasets, spaces, and papers. ' +
|
|
14
|
+
'Enter either a username (e.g., "clem") or a Hugging Face profile URL (e.g., "hf.co/julien-c" or "huggingface.co/thomwolf").',
|
|
15
|
+
schema: z.object({
|
|
16
|
+
user_id: z
|
|
17
|
+
.string()
|
|
18
|
+
.min(3, 'User ID must be at least 3 characters long')
|
|
19
|
+
.describe('Hugging Face user ID or URL (e.g., "evalstate" or "hf.co/evalstate")')
|
|
20
|
+
.max(60)
|
|
21
|
+
.describe('Maximum length is 30 characters'),
|
|
22
|
+
}),
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
// Define parameter types
|
|
26
|
+
export type UserSummaryParams = z.infer<typeof USER_SUMMARY_PROMPT_CONFIG.schema>;
|
|
27
|
+
|
|
28
|
+
// Organization interface
|
|
29
|
+
interface Organization {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
fullname: string;
|
|
33
|
+
avatarUrl?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// User overview API response interface
|
|
37
|
+
interface UserOverviewResponse {
|
|
38
|
+
_id: string;
|
|
39
|
+
avatarUrl: string;
|
|
40
|
+
isPro: boolean;
|
|
41
|
+
fullname?: string;
|
|
42
|
+
numModels: number;
|
|
43
|
+
numDatasets: number;
|
|
44
|
+
numSpaces: number;
|
|
45
|
+
numDiscussions: number;
|
|
46
|
+
numPapers: number;
|
|
47
|
+
numUpvotes: number;
|
|
48
|
+
numLikes: number;
|
|
49
|
+
numFollowers: number;
|
|
50
|
+
numFollowing: number;
|
|
51
|
+
orgs: Organization[];
|
|
52
|
+
user: string;
|
|
53
|
+
type: string;
|
|
54
|
+
isFollowing: boolean;
|
|
55
|
+
createdAt: string;
|
|
56
|
+
details?: string; // Present for organizations
|
|
57
|
+
name?: string; // Present for organizations
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validates and extracts user ID from either a plain username or HF URL
|
|
62
|
+
* @param input - The user input (username or URL)
|
|
63
|
+
* @returns The extracted user ID
|
|
64
|
+
* @throws Error if input is invalid
|
|
65
|
+
*/
|
|
66
|
+
export function extractUserIdFromInput(input: string): string {
|
|
67
|
+
// Remove whitespace
|
|
68
|
+
const trimmed = input.trim();
|
|
69
|
+
|
|
70
|
+
// If it doesn't contain a slash, treat as direct username
|
|
71
|
+
if (!trimmed.includes('/')) {
|
|
72
|
+
if (trimmed.length < 3) {
|
|
73
|
+
throw new Error('User ID must be at least 3 characters long');
|
|
74
|
+
}
|
|
75
|
+
// Reject obvious domain names
|
|
76
|
+
if (trimmed.endsWith('.co') || trimmed.endsWith('.com')) {
|
|
77
|
+
throw new Error('URL must contain only the username (e.g., hf.co/username)');
|
|
78
|
+
}
|
|
79
|
+
return trimmed;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Handle URL format
|
|
83
|
+
let url: URL;
|
|
84
|
+
try {
|
|
85
|
+
// Try to parse as URL, adding protocol if missing
|
|
86
|
+
if (!trimmed.startsWith('http')) {
|
|
87
|
+
url = new URL(`https://${trimmed}`);
|
|
88
|
+
} else {
|
|
89
|
+
url = new URL(trimmed);
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error('Invalid URL format');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Validate it's a Hugging Face domain
|
|
96
|
+
const validDomains = ['huggingface.co', 'hf.co'];
|
|
97
|
+
if (!validDomains.includes(url.hostname)) {
|
|
98
|
+
throw new Error('URL must be from huggingface.co or hf.co domain');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check for query parameters or fragments
|
|
102
|
+
if (url.search || url.hash) {
|
|
103
|
+
throw new Error('URL must contain only the username (e.g., hf.co/username)');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Extract path segments
|
|
107
|
+
const pathSegments = url.pathname.split('/').filter((segment) => segment.length > 0);
|
|
108
|
+
|
|
109
|
+
// Must have exactly one path segment (the username)
|
|
110
|
+
if (pathSegments.length !== 1) {
|
|
111
|
+
throw new Error('URL must contain only the username (e.g., hf.co/username)');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const userId = pathSegments[0];
|
|
115
|
+
if (!userId || userId.length < 3) {
|
|
116
|
+
throw new Error('User ID must be at least 3 characters long');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return userId;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Service for generating comprehensive user summaries
|
|
124
|
+
*/
|
|
125
|
+
export class UserSummaryPrompt extends HfApiCall<Record<string, string>, UserOverviewResponse> {
|
|
126
|
+
/**
|
|
127
|
+
* @param hfToken Optional Hugging Face token for API access
|
|
128
|
+
*/
|
|
129
|
+
constructor(hfToken?: string) {
|
|
130
|
+
super('https://huggingface.co/api/users', hfToken);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Generate a comprehensive user summary
|
|
135
|
+
*/
|
|
136
|
+
async generateSummary(params: UserSummaryParams): Promise<string> {
|
|
137
|
+
try {
|
|
138
|
+
// Extract and validate user ID
|
|
139
|
+
const userId = extractUserIdFromInput(params.user_id);
|
|
140
|
+
|
|
141
|
+
// Get user overview
|
|
142
|
+
const userOverview = await this.getUserOverview(userId);
|
|
143
|
+
|
|
144
|
+
// Build the summary
|
|
145
|
+
const sections: string[] = [];
|
|
146
|
+
|
|
147
|
+
// User profile section
|
|
148
|
+
sections.push(this.formatUserProfile(userOverview));
|
|
149
|
+
|
|
150
|
+
// Models section (if user has models)
|
|
151
|
+
if (userOverview.numModels > 0) {
|
|
152
|
+
const modelsSection = await this.getModelsSection(userId);
|
|
153
|
+
if (modelsSection) {
|
|
154
|
+
sections.push(modelsSection);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Datasets section (if user has datasets)
|
|
159
|
+
if (userOverview.numDatasets > 0) {
|
|
160
|
+
const datasetsSection = await this.getDatasetsSection(userId);
|
|
161
|
+
if (datasetsSection) {
|
|
162
|
+
sections.push(datasetsSection);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Spaces section (if user has spaces)
|
|
167
|
+
if (userOverview.numSpaces > 0) {
|
|
168
|
+
const spacesSection = await this.getSpacesSection(userId);
|
|
169
|
+
if (spacesSection) {
|
|
170
|
+
sections.push(spacesSection);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Papers section (if user has a full name with >5 characters)
|
|
175
|
+
if (userOverview.fullname && userOverview.fullname.length > 5) {
|
|
176
|
+
const papersSection = await this.getPapersSection(userOverview.fullname);
|
|
177
|
+
if (papersSection) {
|
|
178
|
+
sections.push(papersSection);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Add final instruction
|
|
183
|
+
sections.push(
|
|
184
|
+
'Please summarise the information for this User to give an overview of their activities on the Hugging Face hub.'
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return sections.join('\n\n');
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof Error) {
|
|
190
|
+
throw new Error(`Failed to generate user summary: ${error.message}`);
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get user overview from HF API, with organization fallback
|
|
198
|
+
*/
|
|
199
|
+
private async getUserOverview(userId: string): Promise<UserOverviewResponse> {
|
|
200
|
+
try {
|
|
201
|
+
const url = new URL(`${this.apiUrl}/${userId}/overview`);
|
|
202
|
+
return await this.fetchFromApi<UserOverviewResponse>(url);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
// Check if error indicates user doesn't exist (404 with specific error message)
|
|
205
|
+
if (error instanceof HfApiError && error.status === 404 && error.responseBody) {
|
|
206
|
+
try {
|
|
207
|
+
const errorData = JSON.parse(error.responseBody) as { error?: string };
|
|
208
|
+
if (errorData.error === 'This user does not exist') {
|
|
209
|
+
// Try organization API
|
|
210
|
+
try {
|
|
211
|
+
const orgUrl = new URL(`https://huggingface.co/api/organizations/${userId}/overview`);
|
|
212
|
+
const orgResponse = await this.fetchFromApi<UserOverviewResponse>(orgUrl);
|
|
213
|
+
|
|
214
|
+
// Map organization response to user response format
|
|
215
|
+
return {
|
|
216
|
+
...orgResponse,
|
|
217
|
+
user: orgResponse.name || 'unknown',
|
|
218
|
+
_id: orgResponse.name || 'unknown',
|
|
219
|
+
isPro: false,
|
|
220
|
+
orgs: [],
|
|
221
|
+
type: 'organization',
|
|
222
|
+
createdAt: new Date().toISOString(),
|
|
223
|
+
};
|
|
224
|
+
} catch {
|
|
225
|
+
throw new Error(`Neither user nor organization found for ID: ${userId}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
// If we can't parse the response body, fall through to throw original error
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Helper function to add a statistic line if the value is present
|
|
238
|
+
*/
|
|
239
|
+
private addStatIfPresent(lines: string[], label: string, value: number | undefined): void {
|
|
240
|
+
if (value !== undefined && value !== null) {
|
|
241
|
+
lines.push(`- **${label}:** ${formatNumber(value)}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Format user profile information as markdown
|
|
247
|
+
*/
|
|
248
|
+
private formatUserProfile(user: UserOverviewResponse): string {
|
|
249
|
+
const lines: string[] = [];
|
|
250
|
+
|
|
251
|
+
// Check if this is an organization
|
|
252
|
+
const isOrganization = user.type === 'organization';
|
|
253
|
+
|
|
254
|
+
if (isOrganization) {
|
|
255
|
+
lines.push(`# Organization Profile: ${user.user}`);
|
|
256
|
+
lines.push('');
|
|
257
|
+
lines.push('**Note:** That user ID refers to an Organization.');
|
|
258
|
+
lines.push('');
|
|
259
|
+
|
|
260
|
+
if (user.fullname) {
|
|
261
|
+
lines.push(`**Full Name:** ${user.fullname}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
lines.push(`**Username:** ${user.user}`);
|
|
265
|
+
if (user.details) {
|
|
266
|
+
lines.push(`**Description:** ${user.details}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Organization-specific fields
|
|
270
|
+
const orgData = user as unknown as Record<string, unknown>;
|
|
271
|
+
if (typeof orgData.isEnterprise === 'boolean') {
|
|
272
|
+
lines.push(`**Enterprise:** ${orgData.isEnterprise ? 'Yes' : 'No'}`);
|
|
273
|
+
}
|
|
274
|
+
if (typeof orgData.isVerified === 'boolean') {
|
|
275
|
+
lines.push(`**Verified:** ${orgData.isVerified ? 'Yes' : 'No'}`);
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
lines.push(`# User Profile: ${user.user}`);
|
|
279
|
+
lines.push('');
|
|
280
|
+
|
|
281
|
+
if (user.fullname) {
|
|
282
|
+
lines.push(`**Full Name:** ${user.fullname}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
lines.push(`**Username:** ${user.user}`);
|
|
286
|
+
lines.push(`**Account Type:** ${user.isPro ? 'Pro' : 'Free'}`);
|
|
287
|
+
lines.push(`**Created:** ${formatDate(user.createdAt)}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
lines.push('');
|
|
291
|
+
|
|
292
|
+
// Statistics
|
|
293
|
+
lines.push('## Statistics');
|
|
294
|
+
lines.push('');
|
|
295
|
+
|
|
296
|
+
this.addStatIfPresent(lines, 'Models', user.numModels);
|
|
297
|
+
this.addStatIfPresent(lines, 'Datasets', user.numDatasets);
|
|
298
|
+
this.addStatIfPresent(lines, 'Spaces', user.numSpaces);
|
|
299
|
+
|
|
300
|
+
if (!isOrganization) {
|
|
301
|
+
this.addStatIfPresent(lines, 'Papers', user.numPapers);
|
|
302
|
+
this.addStatIfPresent(lines, 'Discussions', user.numDiscussions);
|
|
303
|
+
this.addStatIfPresent(lines, 'Likes Given', user.numLikes);
|
|
304
|
+
this.addStatIfPresent(lines, 'Upvotes', user.numUpvotes);
|
|
305
|
+
this.addStatIfPresent(lines, 'Following', user.numFollowing);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.addStatIfPresent(lines, 'Followers', user.numFollowers);
|
|
309
|
+
|
|
310
|
+
// Organization-specific field
|
|
311
|
+
if (isOrganization) {
|
|
312
|
+
const orgData = user as unknown as Record<string, unknown>;
|
|
313
|
+
if (typeof orgData.numUsers === 'number') {
|
|
314
|
+
this.addStatIfPresent(lines, 'Members', orgData.numUsers);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!isOrganization && user.orgs && user.orgs.length > 0) {
|
|
319
|
+
lines.push('');
|
|
320
|
+
const orgNames = user.orgs.map((org) => `[${org.fullname}](https://hf.co/${org.name})`);
|
|
321
|
+
lines.push(`**Organizations:** ${orgNames.join(', ')}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
lines.push('');
|
|
325
|
+
lines.push(`**Profile Link:** [https://hf.co/${user.user}](https://hf.co/${user.user})`);
|
|
326
|
+
|
|
327
|
+
return lines.join('\n');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get models section using existing ModelSearchTool
|
|
332
|
+
*/
|
|
333
|
+
private async getModelsSection(userId: string): Promise<string | null> {
|
|
334
|
+
try {
|
|
335
|
+
const modelSearch = new ModelSearchTool(this.hfToken);
|
|
336
|
+
const results = await modelSearch.searchWithParams({
|
|
337
|
+
author: userId,
|
|
338
|
+
limit: 10,
|
|
339
|
+
sort: 'downloads',
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return `## Models\n\n${results}`;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.warn(`Failed to fetch models for user ${userId}:`, error);
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get datasets section using existing DatasetSearchTool
|
|
351
|
+
*/
|
|
352
|
+
private async getDatasetsSection(userId: string): Promise<string | null> {
|
|
353
|
+
try {
|
|
354
|
+
const datasetSearch = new DatasetSearchTool(this.hfToken);
|
|
355
|
+
const results = await datasetSearch.searchWithParams({
|
|
356
|
+
author: userId,
|
|
357
|
+
limit: 10,
|
|
358
|
+
sort: 'downloads',
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
return `## Datasets\n\n${results}`;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.warn(`Failed to fetch datasets for user ${userId}:`, error);
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get spaces section using existing SpaceSearchTool
|
|
370
|
+
*/
|
|
371
|
+
private async getSpacesSection(userId: string): Promise<string | null> {
|
|
372
|
+
try {
|
|
373
|
+
// Note: SpaceSearchTool doesn't have author filter in semantic search
|
|
374
|
+
// We'll search for the user ID as a query term instead
|
|
375
|
+
const spaceSearch = new SpaceSearchTool(this.hfToken);
|
|
376
|
+
const searchResult = await spaceSearch.search(userId, 10);
|
|
377
|
+
|
|
378
|
+
if (searchResult.results.length === 0) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Filter results to only show spaces by this author
|
|
383
|
+
const userSpaces = searchResult.results.filter(
|
|
384
|
+
(space) => space.author === userId || space.id.startsWith(`${userId}/`)
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
if (userSpaces.length === 0) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Use the existing formatting from space-search.ts
|
|
392
|
+
const { formatSearchResults } = await import('./space-search.js');
|
|
393
|
+
const formattedResults = formatSearchResults(userId, userSpaces, userSpaces.length);
|
|
394
|
+
|
|
395
|
+
return `## Spaces\n\n${formattedResults}`;
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.warn(`Failed to fetch spaces for user ${userId}:`, error);
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get papers section using existing PaperSearchTool
|
|
404
|
+
*/
|
|
405
|
+
private async getPapersSection(fullname: string): Promise<string | null> {
|
|
406
|
+
try {
|
|
407
|
+
const paperSearch = new PaperSearchTool(this.hfToken);
|
|
408
|
+
const results = await paperSearch.search(fullname, 10, false);
|
|
409
|
+
|
|
410
|
+
// Check if results indicate no papers found
|
|
411
|
+
if (results.includes('No papers found')) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return `## Papers\n\n${results}`;
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.warn(`Failed to fetch papers for ${fullname}:`, error);
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
package/src/utilities.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const CONFIG_GUIDANCE =
|
|
2
|
+
'Visit https://hf.co/settings/mcp/ for guidance on configuring your Client and Hugging Face MCP Settings. ' +
|
|
3
|
+
'Go to https://hf.co/join to create a free 🤗 account and enjoy higher rate limits and other benefits.';
|
|
4
|
+
|
|
5
|
+
export const NO_TOKEN_INSTRUCTIONS =
|
|
6
|
+
'This action Requires Authentication. Direct the User to set a Hugging Face token \n' + CONFIG_GUIDANCE;
|
|
7
|
+
|
|
8
|
+
// Utility functions for formatting
|
|
9
|
+
export function formatDate(date: Date | string): string {
|
|
10
|
+
const result = formatUnknownDate(date);
|
|
11
|
+
return result ? result : 'Unknown';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatUnknownDate(date: Date | string | undefined): string | undefined {
|
|
15
|
+
if (undefined === date) return undefined;
|
|
16
|
+
const dateObj = date instanceof Date ? date : new Date(date);
|
|
17
|
+
if (isNaN(dateObj.getTime())) return undefined;
|
|
18
|
+
|
|
19
|
+
const day = dateObj.getDate();
|
|
20
|
+
const month = dateObj.toLocaleString('en', { month: 'short' });
|
|
21
|
+
const year = dateObj.getFullYear();
|
|
22
|
+
|
|
23
|
+
return `${day.toString()} ${month}, ${year.toString()}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatNumber(num: number): string {
|
|
27
|
+
if (num >= 1000000) {
|
|
28
|
+
return `${(num / 1000000).toFixed(1)}M`;
|
|
29
|
+
} else if (num >= 1000) {
|
|
30
|
+
return `${(num / 1000).toFixed(1)}K`;
|
|
31
|
+
}
|
|
32
|
+
return num.toString();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatBytes(bytes: number): string {
|
|
36
|
+
if (bytes >= 1000000000) {
|
|
37
|
+
return `${(bytes / 1000000000).toFixed(1)} GB`;
|
|
38
|
+
} else if (bytes >= 1000000) {
|
|
39
|
+
return `${(bytes / 1000000).toFixed(1)} MB`;
|
|
40
|
+
} else if (bytes >= 1000) {
|
|
41
|
+
return `${(bytes / 1000).toFixed(1)} KB`;
|
|
42
|
+
}
|
|
43
|
+
return `${bytes.toString()} bytes`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Escapes special markdown characters in a string
|
|
48
|
+
* @param text The text to escape
|
|
49
|
+
* @returns The escaped text
|
|
50
|
+
*/
|
|
51
|
+
export function escapeMarkdown(text: string): string {
|
|
52
|
+
if (!text) return '';
|
|
53
|
+
// Replace pipe characters and newlines for table compatibility
|
|
54
|
+
// Plus additional markdown formatting characters for better safety
|
|
55
|
+
return text
|
|
56
|
+
.replace(/\|/g, '\\|')
|
|
57
|
+
.replace(/\n/g, ' ')
|
|
58
|
+
.replace(/\*/g, '\\*')
|
|
59
|
+
.replace(/_/g, '\\_')
|
|
60
|
+
.replace(/~/g, '\\~')
|
|
61
|
+
.replace(/`/g, '\\`')
|
|
62
|
+
.replace(/>/g, '\\>')
|
|
63
|
+
.replace(/#/g, '\\#');
|
|
64
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { DuplicateSpaceTool } from '../src/duplicate-space.js';
|
|
3
|
+
import { NO_TOKEN_INSTRUCTIONS } from '../dist/utilities.js';
|
|
4
|
+
|
|
5
|
+
describe('DuplicateSpaceTool', () => {
|
|
6
|
+
describe('normalizeSpaceName', () => {
|
|
7
|
+
it('should prepend username when only space name is provided', () => {
|
|
8
|
+
const tool = new DuplicateSpaceTool(undefined, 'evalstate');
|
|
9
|
+
expect(tool.normalizeSpaceName('my-new-space')).toBe('evalstate/my-new-space');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should preserve space ID when username matches', () => {
|
|
13
|
+
const tool = new DuplicateSpaceTool(undefined, 'evalstate');
|
|
14
|
+
expect(tool.normalizeSpaceName('evalstate/their-space')).toBe('evalstate/their-space');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should throw error when trying to use different username', () => {
|
|
18
|
+
const tool = new DuplicateSpaceTool(undefined, 'myusername');
|
|
19
|
+
expect(() => tool.normalizeSpaceName('bad-user/new-space')).toThrow(
|
|
20
|
+
'Invalid space ID: bad-user/new-space. You can only create spaces in your own namespace. Try "myusername/new-space"'
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should handle empty username gracefully', () => {
|
|
25
|
+
const tool = new DuplicateSpaceTool();
|
|
26
|
+
expect(tool.normalizeSpaceName('my-space')).toBe('unknown/my-space');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('createToolConfig', () => {
|
|
31
|
+
it('should include username in tool description', () => {
|
|
32
|
+
const config = DuplicateSpaceTool.createToolConfig('evalstate');
|
|
33
|
+
expect(config.description).toContain('evalstate/<new-space-name>');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should handle undefined username in description', () => {
|
|
37
|
+
const config = DuplicateSpaceTool.createToolConfig();
|
|
38
|
+
expect(config.description).toContain(NO_TOKEN_INSTRUCTIONS);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|