@llmindset/hf-mcp 0.2.33 → 0.2.35
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/dist/docs-search/docs-semantic-search.js +1 -1
- package/dist/docs-search/docs-semantic-search.js.map +1 -1
- package/dist/hf-api-call.d.ts.map +1 -1
- package/dist/hf-api-call.js +4 -0
- package/dist/hf-api-call.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/jobs/api-client.d.ts +19 -0
- package/dist/jobs/api-client.d.ts.map +1 -0
- package/dist/jobs/api-client.js +95 -0
- package/dist/jobs/api-client.js.map +1 -0
- package/dist/jobs/commands/inspect.d.ts +5 -0
- package/dist/jobs/commands/inspect.d.ts.map +1 -0
- package/dist/jobs/commands/inspect.js +21 -0
- package/dist/jobs/commands/inspect.js.map +1 -0
- package/dist/jobs/commands/logs.d.ts +4 -0
- package/dist/jobs/commands/logs.d.ts.map +1 -0
- package/dist/jobs/commands/logs.js +24 -0
- package/dist/jobs/commands/logs.js.map +1 -0
- package/dist/jobs/commands/ps.d.ts +4 -0
- package/dist/jobs/commands/ps.d.ts.map +1 -0
- package/dist/jobs/commands/ps.js +23 -0
- package/dist/jobs/commands/ps.js.map +1 -0
- package/dist/jobs/commands/run.d.ts +5 -0
- package/dist/jobs/commands/run.d.ts.map +1 -0
- package/dist/jobs/commands/run.js +90 -0
- package/dist/jobs/commands/run.js.map +1 -0
- package/dist/jobs/commands/scheduled.d.ts +10 -0
- package/dist/jobs/commands/scheduled.d.ts.map +1 -0
- package/dist/jobs/commands/scheduled.js +112 -0
- package/dist/jobs/commands/scheduled.js.map +1 -0
- package/dist/jobs/commands/utils.d.ts +20 -0
- package/dist/jobs/commands/utils.d.ts.map +1 -0
- package/dist/jobs/commands/utils.js +120 -0
- package/dist/jobs/commands/utils.js.map +1 -0
- package/dist/jobs/formatters.d.ts +6 -0
- package/dist/jobs/formatters.d.ts.map +1 -0
- package/dist/jobs/formatters.js +98 -0
- package/dist/jobs/formatters.js.map +1 -0
- package/dist/jobs/sse-handler.d.ts +12 -0
- package/dist/jobs/sse-handler.d.ts.map +1 -0
- package/dist/jobs/sse-handler.js +80 -0
- package/dist/jobs/sse-handler.js.map +1 -0
- package/dist/jobs/tool.d.ts +35 -0
- package/dist/jobs/tool.d.ts.map +1 -0
- package/dist/jobs/tool.js +333 -0
- package/dist/jobs/tool.js.map +1 -0
- package/dist/jobs/types.d.ts +295 -0
- package/dist/jobs/types.d.ts.map +1 -0
- package/dist/jobs/types.js +95 -0
- package/dist/jobs/types.js.map +1 -0
- package/dist/tool-ids.d.ts +3 -2
- package/dist/tool-ids.d.ts.map +1 -1
- package/dist/tool-ids.js +10 -2
- package/dist/tool-ids.js.map +1 -1
- package/dist/types/tool-result.d.ts +1 -0
- package/dist/types/tool-result.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/docs-search/docs-semantic-search.ts +1 -1
- package/src/hf-api-call.ts +6 -0
- package/src/index.ts +1 -0
- package/src/jobs/api-client.ts +187 -0
- package/src/jobs/commands/inspect.ts +38 -0
- package/src/jobs/commands/logs.ts +36 -0
- package/src/jobs/commands/ps.ts +40 -0
- package/src/jobs/commands/run.ts +135 -0
- package/src/jobs/commands/scheduled.ts +198 -0
- package/src/jobs/commands/utils.ts +191 -0
- package/src/jobs/formatters.ts +149 -0
- package/src/jobs/sse-handler.ts +144 -0
- package/src/jobs/tool.ts +435 -0
- package/src/jobs/types.ts +237 -0
- package/src/tool-ids.ts +11 -1
- package/src/types/tool-result.ts +6 -0
- package/test/jobs/command-translation.spec.ts +331 -0
- package/test/jobs/formatters.spec.ts +267 -0
- package/test/jobs/uv-command.spec.ts +81 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { LogEvent } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for fetching logs via SSE
|
|
5
|
+
*/
|
|
6
|
+
export interface SseLogOptions {
|
|
7
|
+
/** Maximum time to collect logs in milliseconds (default: 10000 = 10s) */
|
|
8
|
+
maxDuration?: number;
|
|
9
|
+
/** Maximum number of lines to return (default: 20) */
|
|
10
|
+
maxLines?: number;
|
|
11
|
+
/** HF API token for authentication */
|
|
12
|
+
token?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Result from fetching logs
|
|
17
|
+
*/
|
|
18
|
+
export interface SseLogResult {
|
|
19
|
+
/** Log lines collected */
|
|
20
|
+
logs: string[];
|
|
21
|
+
/** Whether the job finished during collection */
|
|
22
|
+
finished: boolean;
|
|
23
|
+
/** Whether collection was truncated due to timeout */
|
|
24
|
+
truncated: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fetch logs from a job via Server-Sent Events (SSE)
|
|
29
|
+
* Collects logs for a maximum duration and returns the last N lines
|
|
30
|
+
*
|
|
31
|
+
* @param url - The SSE endpoint URL for job logs
|
|
32
|
+
* @param options - Options for log collection
|
|
33
|
+
* @returns Log result with collected lines and status
|
|
34
|
+
*/
|
|
35
|
+
export async function fetchJobLogs(url: string, options: SseLogOptions = {}): Promise<SseLogResult> {
|
|
36
|
+
const { maxDuration = 10000, maxLines = 20, token } = options;
|
|
37
|
+
|
|
38
|
+
const logLines: string[] = [];
|
|
39
|
+
let finished = false;
|
|
40
|
+
let truncated = false;
|
|
41
|
+
|
|
42
|
+
// Create abort controller for timeout
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeoutId = setTimeout(() => {
|
|
45
|
+
controller.abort();
|
|
46
|
+
truncated = true;
|
|
47
|
+
}, maxDuration);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const headers: Record<string, string> = {
|
|
51
|
+
Accept: 'text/event-stream',
|
|
52
|
+
};
|
|
53
|
+
if (token) {
|
|
54
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const response = await fetch(url, {
|
|
58
|
+
headers,
|
|
59
|
+
signal: controller.signal,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Failed to fetch logs: ${response.status} ${response.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!response.body) {
|
|
67
|
+
throw new Error('Response body is null');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Process the SSE stream
|
|
71
|
+
const reader = response.body.getReader();
|
|
72
|
+
const decoder = new TextDecoder();
|
|
73
|
+
let buffer = '';
|
|
74
|
+
|
|
75
|
+
while (true) {
|
|
76
|
+
const { done, value } = await reader.read();
|
|
77
|
+
|
|
78
|
+
if (done) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Decode chunk and add to buffer
|
|
83
|
+
buffer += decoder.decode(value, { stream: true });
|
|
84
|
+
|
|
85
|
+
// Process complete lines
|
|
86
|
+
const lines = buffer.split('\n');
|
|
87
|
+
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
88
|
+
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
// SSE format: "data: {json}"
|
|
91
|
+
if (line.startsWith('data: ')) {
|
|
92
|
+
try {
|
|
93
|
+
const jsonStr = line.substring(6); // Remove "data: " prefix
|
|
94
|
+
const event = JSON.parse(jsonStr) as LogEvent;
|
|
95
|
+
|
|
96
|
+
// Filter out system messages
|
|
97
|
+
if (event.data.startsWith('===== Job started')) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check for job finished message
|
|
102
|
+
if (event.data.startsWith('===== Job finished')) {
|
|
103
|
+
finished = true;
|
|
104
|
+
// Extract status from message if present
|
|
105
|
+
// e.g., "===== Job finished: status=COMPLETED ====="
|
|
106
|
+
logLines.push(event.data);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Add log line
|
|
111
|
+
logLines.push(event.data);
|
|
112
|
+
} catch {
|
|
113
|
+
// Ignore malformed JSON
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Break if job finished
|
|
120
|
+
if (finished) {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Close the reader
|
|
126
|
+
await reader.cancel();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// If aborted due to timeout, that's expected
|
|
129
|
+
if ((error as Error).name !== 'AbortError') {
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
} finally {
|
|
133
|
+
clearTimeout(timeoutId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Return last N lines
|
|
137
|
+
const lastLines = logLines.slice(-maxLines);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
logs: lastLines,
|
|
141
|
+
finished,
|
|
142
|
+
truncated: truncated && !finished,
|
|
143
|
+
};
|
|
144
|
+
}
|
package/src/jobs/tool.ts
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { JobsApiClient } from './api-client.js';
|
|
3
|
+
import { HfApiError } from '../hf-api-call.js';
|
|
4
|
+
import { runCommand, uvCommand } from './commands/run.js';
|
|
5
|
+
import { psCommand } from './commands/ps.js';
|
|
6
|
+
import { logsCommand } from './commands/logs.js';
|
|
7
|
+
import { inspectCommand, cancelCommand } from './commands/inspect.js';
|
|
8
|
+
import {
|
|
9
|
+
scheduledRunCommand,
|
|
10
|
+
scheduledUvCommand,
|
|
11
|
+
scheduledPsCommand,
|
|
12
|
+
scheduledInspectCommand,
|
|
13
|
+
scheduledDeleteCommand,
|
|
14
|
+
scheduledSuspendCommand,
|
|
15
|
+
scheduledResumeCommand,
|
|
16
|
+
} from './commands/scheduled.js';
|
|
17
|
+
import type { ToolResult } from '../types/tool-result.js';
|
|
18
|
+
import type {
|
|
19
|
+
RunArgs,
|
|
20
|
+
UvArgs,
|
|
21
|
+
PsArgs,
|
|
22
|
+
LogsArgs,
|
|
23
|
+
InspectArgs,
|
|
24
|
+
CancelArgs,
|
|
25
|
+
ScheduledRunArgs,
|
|
26
|
+
ScheduledUvArgs,
|
|
27
|
+
ScheduledPsArgs,
|
|
28
|
+
ScheduledJobArgs,
|
|
29
|
+
} from './types.js';
|
|
30
|
+
|
|
31
|
+
// Re-export types
|
|
32
|
+
export * from './types.js';
|
|
33
|
+
export { JobsApiClient } from './api-client.js';
|
|
34
|
+
|
|
35
|
+
// Import Zod schemas for validation
|
|
36
|
+
import {
|
|
37
|
+
runArgsSchema,
|
|
38
|
+
uvArgsSchema,
|
|
39
|
+
psArgsSchema,
|
|
40
|
+
logsArgsSchema,
|
|
41
|
+
inspectArgsSchema,
|
|
42
|
+
cancelArgsSchema,
|
|
43
|
+
scheduledRunArgsSchema,
|
|
44
|
+
scheduledUvArgsSchema,
|
|
45
|
+
scheduledPsArgsSchema,
|
|
46
|
+
scheduledJobArgsSchema,
|
|
47
|
+
} from './types.js';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Map of command names to their validation schemas
|
|
51
|
+
*/
|
|
52
|
+
const COMMAND_SCHEMAS = {
|
|
53
|
+
run: runArgsSchema,
|
|
54
|
+
uv: uvArgsSchema,
|
|
55
|
+
ps: psArgsSchema,
|
|
56
|
+
logs: logsArgsSchema,
|
|
57
|
+
inspect: inspectArgsSchema,
|
|
58
|
+
cancel: cancelArgsSchema,
|
|
59
|
+
'scheduled run': scheduledRunArgsSchema,
|
|
60
|
+
'scheduled uv': scheduledUvArgsSchema,
|
|
61
|
+
'scheduled ps': scheduledPsArgsSchema,
|
|
62
|
+
'scheduled inspect': scheduledJobArgsSchema,
|
|
63
|
+
'scheduled delete': scheduledJobArgsSchema,
|
|
64
|
+
'scheduled suspend': scheduledJobArgsSchema,
|
|
65
|
+
'scheduled resume': scheduledJobArgsSchema,
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate command arguments against a Zod schema
|
|
70
|
+
* Returns a ToolResult with detailed error message if validation fails
|
|
71
|
+
*/
|
|
72
|
+
function validateArgs(
|
|
73
|
+
schema: z.ZodSchema,
|
|
74
|
+
args: unknown,
|
|
75
|
+
commandName: string
|
|
76
|
+
): { success: true } | { success: false; errorResult: ToolResult } {
|
|
77
|
+
const result = schema.safeParse(args);
|
|
78
|
+
|
|
79
|
+
if (result.success) {
|
|
80
|
+
return { success: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Format Zod errors into a helpful message
|
|
84
|
+
const errors = result.error.errors;
|
|
85
|
+
const missingFields: string[] = [];
|
|
86
|
+
const invalidFields: string[] = [];
|
|
87
|
+
|
|
88
|
+
for (const err of errors) {
|
|
89
|
+
const field = err.path.join('.');
|
|
90
|
+
if (err.code === 'invalid_type' && err.received === 'undefined') {
|
|
91
|
+
missingFields.push(` • ${field}: ${err.message}`);
|
|
92
|
+
} else {
|
|
93
|
+
invalidFields.push(` • ${field}: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let errorMessage = `Error: Invalid parameters for '${commandName}'\n\n`;
|
|
98
|
+
|
|
99
|
+
if (missingFields.length > 0) {
|
|
100
|
+
errorMessage += `Missing required parameters:\n${missingFields.join('\n')}\n\n`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (invalidFields.length > 0) {
|
|
104
|
+
errorMessage += `Invalid parameters:\n${invalidFields.join('\n')}\n\n`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Show what was provided
|
|
108
|
+
const providedKeys = args && typeof args === 'object' ? Object.keys(args) : [];
|
|
109
|
+
if (providedKeys.length > 0) {
|
|
110
|
+
errorMessage += `You provided: ${JSON.stringify(args, null, 2)}`;
|
|
111
|
+
} else {
|
|
112
|
+
errorMessage += `You provided: {} (no parameters)`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
errorResult: {
|
|
118
|
+
formatted: errorMessage,
|
|
119
|
+
totalResults: 0,
|
|
120
|
+
resultsShared: 0,
|
|
121
|
+
isError: true,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Usage instructions when tool is called with no arguments
|
|
128
|
+
*/
|
|
129
|
+
const USAGE_INSTRUCTIONS = `# HuggingFace Jobs API
|
|
130
|
+
|
|
131
|
+
Manage compute jobs on Hugging Face infrastructure.
|
|
132
|
+
|
|
133
|
+
## Available Commands
|
|
134
|
+
|
|
135
|
+
### Job Management
|
|
136
|
+
- **run** - Run a job with a Docker image
|
|
137
|
+
- **uv** - Run a Python script with UV (inline dependencies)
|
|
138
|
+
- **ps** - List jobs
|
|
139
|
+
- **logs** - Fetch job logs
|
|
140
|
+
- **inspect** - Get detailed job information
|
|
141
|
+
- **cancel** - Cancel a running job
|
|
142
|
+
|
|
143
|
+
### Scheduled Jobs
|
|
144
|
+
- **scheduled run** - Create a scheduled job
|
|
145
|
+
- **scheduled uv** - Create a scheduled UV job
|
|
146
|
+
- **scheduled ps** - List scheduled jobs
|
|
147
|
+
- **scheduled inspect** - Get scheduled job details
|
|
148
|
+
- **scheduled delete** - Delete a scheduled job
|
|
149
|
+
- **scheduled suspend** - Pause a scheduled job
|
|
150
|
+
- **scheduled resume** - Resume a suspended job
|
|
151
|
+
|
|
152
|
+
## Examples
|
|
153
|
+
|
|
154
|
+
### Run a simple job
|
|
155
|
+
\`\`\`
|
|
156
|
+
# Command as array (recommended, especially for complex commands)
|
|
157
|
+
hf_jobs("run", {
|
|
158
|
+
"image": "python:3.12",
|
|
159
|
+
"command": ["python", "-c", "print('Hello from HF Jobs!')"],
|
|
160
|
+
"flavor": "cpu-basic"
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
# Command as string (parsed with POSIX shell semantics)
|
|
164
|
+
hf_jobs("run", {
|
|
165
|
+
"image": "python:3.12",
|
|
166
|
+
"command": "python -c \\"print('Hello world!')\\"",
|
|
167
|
+
"flavor": "cpu-basic"
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
# Use a Hugging Face Space as the image
|
|
171
|
+
hf_jobs("run", {
|
|
172
|
+
"image": "hf.co/spaces/username/spacename",
|
|
173
|
+
"command": ["python", "app.py"],
|
|
174
|
+
"flavor": "cpu-basic"
|
|
175
|
+
})
|
|
176
|
+
\`\`\`
|
|
177
|
+
|
|
178
|
+
### Run multiline Python scripts
|
|
179
|
+
\`\`\`
|
|
180
|
+
# Use array format with newlines in the -c argument
|
|
181
|
+
hf_jobs("run", {
|
|
182
|
+
"image": "python:3.12",
|
|
183
|
+
"command": ["python", "-c", "import sys\\nprint('Line 1')\\nprint('Line 2')"],
|
|
184
|
+
"flavor": "cpu-basic"
|
|
185
|
+
})
|
|
186
|
+
\`\`\`
|
|
187
|
+
|
|
188
|
+
### Run bash/shell commands
|
|
189
|
+
\`\`\`
|
|
190
|
+
hf_jobs("run", {
|
|
191
|
+
"image": "ubuntu:22.04",
|
|
192
|
+
"command": ["bash", "-c", "apt-get update && apt-get install -y curl"],
|
|
193
|
+
"flavor": "cpu-basic"
|
|
194
|
+
})
|
|
195
|
+
\`\`\`
|
|
196
|
+
|
|
197
|
+
### List running jobs
|
|
198
|
+
\`\`\`
|
|
199
|
+
hf_jobs("ps")
|
|
200
|
+
\`\`\`
|
|
201
|
+
|
|
202
|
+
### Get job logs
|
|
203
|
+
\`\`\`
|
|
204
|
+
hf_jobs("logs", {"job_id": "your-job-id"})
|
|
205
|
+
\`\`\`
|
|
206
|
+
|
|
207
|
+
### Run with GPU
|
|
208
|
+
\`\`\`
|
|
209
|
+
hf_jobs("run", {
|
|
210
|
+
"image": "pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel",
|
|
211
|
+
"command": ["python", "train.py"],
|
|
212
|
+
"flavor": "a10g-small"
|
|
213
|
+
})
|
|
214
|
+
\`\`\`
|
|
215
|
+
|
|
216
|
+
### Schedule a job
|
|
217
|
+
\`\`\`
|
|
218
|
+
hf_jobs("scheduled run", {
|
|
219
|
+
"schedule": "@hourly",
|
|
220
|
+
"image": "python:3.12",
|
|
221
|
+
"command": ["python", "backup.py"]
|
|
222
|
+
})
|
|
223
|
+
\`\`\`
|
|
224
|
+
|
|
225
|
+
## Hardware Flavors
|
|
226
|
+
|
|
227
|
+
**CPU:** cpu-basic, cpu-upgrade, cpu-performance, cpu-xl
|
|
228
|
+
**GPU:** t4-small, t4-medium, l4x1, a10g-small, a10g-large, a100-large, h100
|
|
229
|
+
**Specialized:** inf2x6 (AWS Inferentia)
|
|
230
|
+
|
|
231
|
+
## Command Format Guidelines
|
|
232
|
+
|
|
233
|
+
**Array format (recommended):**
|
|
234
|
+
- Use for complex commands, multiline scripts, or commands with special characters
|
|
235
|
+
- No quoting/escaping needed: \`["python", "-c", "print('hello')"]\`
|
|
236
|
+
- Works with any language: Python, bash, npm, etc.
|
|
237
|
+
|
|
238
|
+
**String format:**
|
|
239
|
+
- Parsed with POSIX shell semantics (quotes, escaping)
|
|
240
|
+
- Good for simple commands: \`"python script.py"\`
|
|
241
|
+
- Shell operators (|, &&, >, etc.) are NOT supported - use array with \`bash -c\` instead
|
|
242
|
+
- \`$HF_TOKEN\` stays literal in the command. To forward your authenticated token, add \`secrets: { "HF_TOKEN": "$HF_TOKEN" }\`.
|
|
243
|
+
|
|
244
|
+
**Multiline inline scripts:**
|
|
245
|
+
- Automatically wrapped in \`["/bin/sh", "-lc", "..."]\` for shell execution
|
|
246
|
+
- Paste entire Python/bash snippets—they execute as if typed in a shell
|
|
247
|
+
|
|
248
|
+
## Tips
|
|
249
|
+
|
|
250
|
+
- Jobs default to detached mode (return immediately with job ID)
|
|
251
|
+
- Use Hub resources directly: \`load_dataset('squad')\`, \`AutoModel.from_pretrained('bert-base')\`
|
|
252
|
+
- To access private Hub assets, include \`secrets: { "HF_TOKEN": "$HF_TOKEN" }\` (or \`${'${HF_TOKEN}'}\`) so the server injects your bearer token.
|
|
253
|
+
- Logs are time-limited (10s max) - check job page for full logs
|
|
254
|
+
- For shell pipes/operators, use: \`["bash", "-c", "cmd1 | cmd2"]\`
|
|
255
|
+
`;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Jobs tool configuration
|
|
259
|
+
*/
|
|
260
|
+
export const HF_JOBS_TOOL_CONFIG = {
|
|
261
|
+
name: 'hf_jobs',
|
|
262
|
+
description:
|
|
263
|
+
'Manage HuggingFace compute jobs. Run any command in Docker containers (Python, Node.js, bash, etc.), ' +
|
|
264
|
+
'execute Python scripts with UV package manager, manage scheduled jobs, and monitor job status and logs. ' +
|
|
265
|
+
'Supports CPU and GPU hardware. Call with no arguments for full usage instructions and examples.',
|
|
266
|
+
schema: z.object({
|
|
267
|
+
command: z
|
|
268
|
+
.string()
|
|
269
|
+
.optional()
|
|
270
|
+
.describe(
|
|
271
|
+
'Command to execute: "run", "uv", "ps", "logs", "inspect", "cancel", ' +
|
|
272
|
+
'"scheduled run", "scheduled uv", "scheduled ps", "scheduled inspect", ' +
|
|
273
|
+
'"scheduled delete", "scheduled suspend", "scheduled resume"'
|
|
274
|
+
),
|
|
275
|
+
args: z.record(z.any()).optional().describe('Command-specific arguments as a JSON object'),
|
|
276
|
+
}),
|
|
277
|
+
annotations: {
|
|
278
|
+
title: 'HuggingFace Jobs',
|
|
279
|
+
destructiveHint: false,
|
|
280
|
+
readOnlyHint: false,
|
|
281
|
+
openWorldHint: true,
|
|
282
|
+
},
|
|
283
|
+
} as const;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Jobs tool implementation
|
|
287
|
+
*/
|
|
288
|
+
export class HfJobsTool {
|
|
289
|
+
private client: JobsApiClient;
|
|
290
|
+
private hfToken?: string;
|
|
291
|
+
private isAuthenticated: boolean;
|
|
292
|
+
|
|
293
|
+
constructor(hfToken?: string, isAuthenticated?: boolean, namespace?: string) {
|
|
294
|
+
this.hfToken = hfToken;
|
|
295
|
+
this.isAuthenticated = isAuthenticated ?? !!hfToken;
|
|
296
|
+
this.client = new JobsApiClient(hfToken, namespace);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Execute a jobs command
|
|
301
|
+
*/
|
|
302
|
+
async execute(params: { command?: string; args?: Record<string, unknown> }): Promise<ToolResult> {
|
|
303
|
+
// If not authenticated, show upgrade message
|
|
304
|
+
if (!this.isAuthenticated) {
|
|
305
|
+
return {
|
|
306
|
+
formatted:
|
|
307
|
+
'Jobs are available for Pro, Team and Enterprise users. Go to https://huggingface.co/pricing to get started.',
|
|
308
|
+
totalResults: 0,
|
|
309
|
+
resultsShared: 0,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// If no command provided, return usage instructions
|
|
314
|
+
if (!params.command) {
|
|
315
|
+
return {
|
|
316
|
+
formatted: USAGE_INSTRUCTIONS,
|
|
317
|
+
totalResults: 1,
|
|
318
|
+
resultsShared: 1,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const command = params.command.toLowerCase();
|
|
323
|
+
const args = params.args || {};
|
|
324
|
+
|
|
325
|
+
// Validate command arguments if schema exists
|
|
326
|
+
const schema = COMMAND_SCHEMAS[command as keyof typeof COMMAND_SCHEMAS];
|
|
327
|
+
if (schema) {
|
|
328
|
+
const validation = validateArgs(schema, args, command);
|
|
329
|
+
if (!validation.success) {
|
|
330
|
+
return validation.errorResult;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
let result: string;
|
|
336
|
+
|
|
337
|
+
switch (command) {
|
|
338
|
+
case 'run':
|
|
339
|
+
result = await runCommand(args as RunArgs, this.client, this.hfToken);
|
|
340
|
+
break;
|
|
341
|
+
|
|
342
|
+
case 'uv':
|
|
343
|
+
result = await uvCommand(args as UvArgs, this.client, this.hfToken);
|
|
344
|
+
break;
|
|
345
|
+
|
|
346
|
+
case 'ps':
|
|
347
|
+
result = await psCommand(args as PsArgs, this.client);
|
|
348
|
+
break;
|
|
349
|
+
|
|
350
|
+
case 'logs':
|
|
351
|
+
result = await logsCommand(args as LogsArgs, this.client, this.hfToken);
|
|
352
|
+
break;
|
|
353
|
+
|
|
354
|
+
case 'inspect':
|
|
355
|
+
result = await inspectCommand(args as InspectArgs, this.client);
|
|
356
|
+
break;
|
|
357
|
+
|
|
358
|
+
case 'cancel':
|
|
359
|
+
result = await cancelCommand(args as CancelArgs, this.client);
|
|
360
|
+
break;
|
|
361
|
+
|
|
362
|
+
case 'scheduled run':
|
|
363
|
+
result = await scheduledRunCommand(args as ScheduledRunArgs, this.client, this.hfToken);
|
|
364
|
+
break;
|
|
365
|
+
|
|
366
|
+
case 'scheduled uv':
|
|
367
|
+
result = await scheduledUvCommand(args as ScheduledUvArgs, this.client, this.hfToken);
|
|
368
|
+
break;
|
|
369
|
+
|
|
370
|
+
case 'scheduled ps':
|
|
371
|
+
result = await scheduledPsCommand(args as ScheduledPsArgs, this.client);
|
|
372
|
+
break;
|
|
373
|
+
|
|
374
|
+
case 'scheduled inspect':
|
|
375
|
+
result = await scheduledInspectCommand(args as ScheduledJobArgs, this.client);
|
|
376
|
+
break;
|
|
377
|
+
|
|
378
|
+
case 'scheduled delete':
|
|
379
|
+
result = await scheduledDeleteCommand(args as ScheduledJobArgs, this.client);
|
|
380
|
+
break;
|
|
381
|
+
|
|
382
|
+
case 'scheduled suspend':
|
|
383
|
+
result = await scheduledSuspendCommand(args as ScheduledJobArgs, this.client);
|
|
384
|
+
break;
|
|
385
|
+
|
|
386
|
+
case 'scheduled resume':
|
|
387
|
+
result = await scheduledResumeCommand(args as ScheduledJobArgs, this.client);
|
|
388
|
+
break;
|
|
389
|
+
|
|
390
|
+
default:
|
|
391
|
+
return {
|
|
392
|
+
formatted: `Unknown command: "${params.command}"
|
|
393
|
+
|
|
394
|
+
Available commands:
|
|
395
|
+
- run, uv, ps, logs, inspect, cancel
|
|
396
|
+
- scheduled run, scheduled uv, scheduled ps, scheduled inspect, scheduled delete, scheduled suspend, scheduled resume
|
|
397
|
+
|
|
398
|
+
Call hf_jobs() with no arguments for full usage instructions.`,
|
|
399
|
+
totalResults: 0,
|
|
400
|
+
resultsShared: 0,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
formatted: result,
|
|
406
|
+
totalResults: 1,
|
|
407
|
+
resultsShared: 1,
|
|
408
|
+
};
|
|
409
|
+
} catch (error) {
|
|
410
|
+
let errorMessage = error instanceof Error ? error.message : String(error);
|
|
411
|
+
|
|
412
|
+
// If this is an HfApiError with a response body, include it
|
|
413
|
+
if (error instanceof HfApiError && error.responseBody) {
|
|
414
|
+
try {
|
|
415
|
+
// Try to parse and format the response body
|
|
416
|
+
const parsed: unknown = JSON.parse(error.responseBody);
|
|
417
|
+
const formattedBody = JSON.stringify(parsed, null, 2);
|
|
418
|
+
errorMessage += `\n\nServer response:\n${formattedBody}`;
|
|
419
|
+
} catch {
|
|
420
|
+
// If not valid JSON, include raw response (if not too long)
|
|
421
|
+
if (error.responseBody.length < 500) {
|
|
422
|
+
errorMessage += `\n\nServer response: ${error.responseBody}`;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
formatted: `Error executing ${params.command}: ${errorMessage}`,
|
|
429
|
+
totalResults: 0,
|
|
430
|
+
resultsShared: 0,
|
|
431
|
+
isError: true,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|