@llmindset/hf-mcp 0.2.32 → 0.2.34
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.d.ts +2 -2
- package/dist/docs-search/docs-semantic-search.d.ts.map +1 -1
- package/dist/docs-search/docs-semantic-search.js +56 -21
- 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 +104 -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 +89 -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 +111 -0
- package/dist/jobs/commands/scheduled.js.map +1 -0
- package/dist/jobs/commands/utils.d.ts +19 -0
- package/dist/jobs/commands/utils.d.ts.map +1 -0
- package/dist/jobs/commands/utils.js +99 -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/dist/use-space.d.ts +0 -1
- package/dist/use-space.d.ts.map +1 -1
- package/dist/use-space.js +0 -1
- package/dist/use-space.js.map +1 -1
- package/package.json +4 -2
- package/src/docs-search/docs-semantic-search.ts +71 -20
- package/src/hf-api-call.ts +6 -0
- package/src/index.ts +1 -0
- package/src/jobs/api-client.ts +195 -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 +134 -0
- package/src/jobs/commands/scheduled.ts +189 -0
- package/src/jobs/commands/utils.ts +158 -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/src/use-space.ts +0 -1
- package/test/jobs/command-translation.spec.ts +277 -0
- package/test/jobs/formatters.spec.ts +267 -0
- package/test/jobs/uv-command.spec.ts +81 -0
- package/dist/types/mcp-ui-server-shim.d.ts +0 -37
- package/dist/types/mcp-ui-server-shim.d.ts.map +0 -1
- package/dist/types/mcp-ui-server-shim.js +0 -2
- package/dist/types/mcp-ui-server-shim.js.map +0 -1
- package/src/types/mcp-ui-server-shim.ts +0 -35
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { InspectArgs, CancelArgs } from '../types.js';
|
|
2
|
+
import type { JobsApiClient } from '../api-client.js';
|
|
3
|
+
import { formatJobDetails } from '../formatters.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Execute the 'inspect' command
|
|
7
|
+
* Gets detailed information about one or more jobs
|
|
8
|
+
*/
|
|
9
|
+
export async function inspectCommand(args: InspectArgs, client: JobsApiClient): Promise<string> {
|
|
10
|
+
const jobIds = Array.isArray(args.job_id) ? args.job_id : [args.job_id];
|
|
11
|
+
|
|
12
|
+
// Fetch all jobs
|
|
13
|
+
const jobs = await Promise.all(
|
|
14
|
+
jobIds.map(async (id) => {
|
|
15
|
+
try {
|
|
16
|
+
return await client.getJob(id, args.namespace);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new Error(`Failed to fetch job ${id}: ${(error as Error).message}`);
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const formattedDetails = formatJobDetails(jobs);
|
|
24
|
+
|
|
25
|
+
return `**Job Details** (${jobs.length} job${jobs.length > 1 ? 's' : ''}):\n\n${formattedDetails}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Execute the 'cancel' command
|
|
30
|
+
* Cancels a running job
|
|
31
|
+
*/
|
|
32
|
+
export async function cancelCommand(args: CancelArgs, client: JobsApiClient): Promise<string> {
|
|
33
|
+
await client.cancelJob(args.job_id, args.namespace);
|
|
34
|
+
|
|
35
|
+
return `✓ Job ${args.job_id} has been cancelled.
|
|
36
|
+
|
|
37
|
+
To verify: \`hf_jobs("inspect", {"job_id": "${args.job_id}"})\``;
|
|
38
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { LogsArgs } from '../types.js';
|
|
2
|
+
import type { JobsApiClient } from '../api-client.js';
|
|
3
|
+
import { fetchJobLogs } from '../sse-handler.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Execute the 'logs' command
|
|
7
|
+
* Fetches logs from a job via SSE
|
|
8
|
+
*/
|
|
9
|
+
export async function logsCommand(args: LogsArgs, client: JobsApiClient, token?: string): Promise<string> {
|
|
10
|
+
// Get namespace for the logs URL
|
|
11
|
+
const namespace = await client.getNamespace(args.namespace);
|
|
12
|
+
const logsUrl = client.getLogsUrl(args.job_id, namespace);
|
|
13
|
+
|
|
14
|
+
// Fetch logs with timeout and line limit
|
|
15
|
+
const result = await fetchJobLogs(logsUrl, {
|
|
16
|
+
token,
|
|
17
|
+
maxDuration: 10000,
|
|
18
|
+
maxLines: args.tail,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (result.logs.length === 0) {
|
|
22
|
+
return `No logs available for job ${args.job_id}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let response = `**Logs for job ${args.job_id}** (last ${args.tail} lines):\n\n${'```'}\n`;
|
|
26
|
+
response += result.logs.join('\n');
|
|
27
|
+
response += `\n${'```'}`;
|
|
28
|
+
|
|
29
|
+
if (result.finished) {
|
|
30
|
+
response += '\n\n✓ Job finished.';
|
|
31
|
+
} else if (result.truncated) {
|
|
32
|
+
response += '\n\n⚠ Log collection stopped after 10 seconds. Job may still be running.';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return response;
|
|
36
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PsArgs } from '../types.js';
|
|
2
|
+
import type { JobsApiClient } from '../api-client.js';
|
|
3
|
+
import { formatJobsTable } from '../formatters.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Execute the 'ps' command
|
|
7
|
+
* Lists jobs with optional filtering
|
|
8
|
+
*/
|
|
9
|
+
export async function psCommand(args: PsArgs, client: JobsApiClient): Promise<string> {
|
|
10
|
+
// Fetch all jobs from API
|
|
11
|
+
const allJobs = await client.listJobs(args.namespace);
|
|
12
|
+
|
|
13
|
+
// Filter jobs
|
|
14
|
+
let jobs = allJobs;
|
|
15
|
+
|
|
16
|
+
// Default: show only running jobs unless --all is specified
|
|
17
|
+
if (!args.all) {
|
|
18
|
+
jobs = jobs.filter((job) => job.status.stage === 'RUNNING');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Apply status filter if specified
|
|
22
|
+
if (args.status) {
|
|
23
|
+
const statusFilter = args.status.toUpperCase();
|
|
24
|
+
jobs = jobs.filter((job) => job.status.stage.toUpperCase().includes(statusFilter));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Format as markdown table
|
|
28
|
+
const table = formatJobsTable(jobs);
|
|
29
|
+
|
|
30
|
+
if (jobs.length === 0) {
|
|
31
|
+
if (args.all) {
|
|
32
|
+
return 'No jobs found.';
|
|
33
|
+
}
|
|
34
|
+
return 'No running jobs found. Use `{"all": true}` to show all jobs.';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return `**Jobs (${jobs.length} of ${allJobs.length} total):**
|
|
38
|
+
|
|
39
|
+
${table}`;
|
|
40
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { RunArgs, UvArgs } from '../types.js';
|
|
2
|
+
import type { JobsApiClient } from '../api-client.js';
|
|
3
|
+
import { createJobSpec } from './utils.js';
|
|
4
|
+
import { fetchJobLogs } from '../sse-handler.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Execute the 'run' command
|
|
8
|
+
* Creates and runs a job, optionally waiting for logs
|
|
9
|
+
*/
|
|
10
|
+
export async function runCommand(args: RunArgs, client: JobsApiClient, token?: string): Promise<string> {
|
|
11
|
+
// Create job spec from args
|
|
12
|
+
const jobSpec = createJobSpec({
|
|
13
|
+
image: args.image,
|
|
14
|
+
command: args.command,
|
|
15
|
+
flavor: args.flavor,
|
|
16
|
+
env: args.env,
|
|
17
|
+
secrets: args.secrets,
|
|
18
|
+
timeout: args.timeout,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Submit job
|
|
22
|
+
const job = await client.runJob(jobSpec, args.namespace);
|
|
23
|
+
|
|
24
|
+
const jobUrl = `https://huggingface.co/jobs/${job.owner.name}/${job.id}`;
|
|
25
|
+
|
|
26
|
+
// If detached, return immediately
|
|
27
|
+
if (args.detach) {
|
|
28
|
+
return `Job started successfully!
|
|
29
|
+
|
|
30
|
+
**Job ID:** ${job.id}
|
|
31
|
+
**Status:** ${job.status.stage}
|
|
32
|
+
**View at:** ${jobUrl}
|
|
33
|
+
|
|
34
|
+
To check logs: \`hf_jobs("logs", {"job_id": "${job.id}"})\`
|
|
35
|
+
To inspect: \`hf_jobs("inspect", {"job_id": "${job.id}"})\``;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Not detached - fetch logs
|
|
39
|
+
const logsUrl = client.getLogsUrl(job.id, job.owner.name);
|
|
40
|
+
const logResult = await fetchJobLogs(logsUrl, { token, maxDuration: 10000, maxLines: 20 });
|
|
41
|
+
|
|
42
|
+
let response = `Job started: ${job.id}\n\n`;
|
|
43
|
+
|
|
44
|
+
if (logResult.logs.length > 0) {
|
|
45
|
+
response += '**Logs (last 20 lines):**\n```\n';
|
|
46
|
+
response += logResult.logs.join('\n');
|
|
47
|
+
response += '\n```\n\n';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (logResult.finished) {
|
|
51
|
+
response += `Job finished. Full details: ${jobUrl}`;
|
|
52
|
+
} else if (logResult.truncated) {
|
|
53
|
+
response += `Log collection stopped after 10s. Job may still be running.\n`;
|
|
54
|
+
response += `View full logs: ${jobUrl}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return response;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Execute the 'uv' command
|
|
62
|
+
* Creates and runs a UV-based Python job
|
|
63
|
+
*/
|
|
64
|
+
export async function uvCommand(args: UvArgs, client: JobsApiClient, token?: string): Promise<string> {
|
|
65
|
+
// UV jobs use a standard UV image unless overridden
|
|
66
|
+
const image = 'ghcr.io/astral-sh/uv:latest'; // Standard UV image
|
|
67
|
+
|
|
68
|
+
// Detect script source and build command
|
|
69
|
+
const scriptSource = args.script;
|
|
70
|
+
let command: string | string[];
|
|
71
|
+
|
|
72
|
+
// Check if script is a URL
|
|
73
|
+
if (scriptSource.startsWith('http://') || scriptSource.startsWith('https://')) {
|
|
74
|
+
// URL - download and run
|
|
75
|
+
command = buildUvCommand(scriptSource, args);
|
|
76
|
+
} else if (scriptSource.includes('\n')) {
|
|
77
|
+
// Inline multi-line script - encode it
|
|
78
|
+
const encoded = Buffer.from(scriptSource).toString('base64');
|
|
79
|
+
const depsPart =
|
|
80
|
+
args.with_deps && args.with_deps.length > 0
|
|
81
|
+
? args.with_deps.map(dep => `--with ${dep}`).join(' ')
|
|
82
|
+
: '';
|
|
83
|
+
const pythonPart = args.python ? `-p ${args.python}` : '';
|
|
84
|
+
const uvArgs = [depsPart, pythonPart].filter(Boolean).join(' ');
|
|
85
|
+
const shellSnippet = `echo "${encoded}" | base64 -d | uv run${uvArgs ? ` ${uvArgs}` : ''} -`;
|
|
86
|
+
command = ['/bin/sh', '-lc', shellSnippet];
|
|
87
|
+
} else {
|
|
88
|
+
// Assume it's a URL or path - UV will handle it
|
|
89
|
+
command = buildUvCommand(scriptSource, args);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Convert to run args
|
|
93
|
+
const runArgs: RunArgs = {
|
|
94
|
+
image,
|
|
95
|
+
command,
|
|
96
|
+
flavor: args.flavor,
|
|
97
|
+
env: args.env,
|
|
98
|
+
secrets: args.secrets,
|
|
99
|
+
timeout: args.timeout,
|
|
100
|
+
detach: args.detach,
|
|
101
|
+
namespace: args.namespace,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return runCommand(runArgs, client, token);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build UV command with options
|
|
109
|
+
*/
|
|
110
|
+
function buildUvCommand(script: string, args: UvArgs): string {
|
|
111
|
+
const parts: string[] = ['uv', 'run'];
|
|
112
|
+
|
|
113
|
+
// Add dependencies
|
|
114
|
+
if (args.with_deps && args.with_deps.length > 0) {
|
|
115
|
+
for (const dep of args.with_deps) {
|
|
116
|
+
parts.push('--with', dep);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Add Python version
|
|
121
|
+
if (args.python) {
|
|
122
|
+
parts.push('-p', args.python);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Add script
|
|
126
|
+
parts.push(script);
|
|
127
|
+
|
|
128
|
+
// Add script arguments
|
|
129
|
+
if (args.script_args && args.script_args.length > 0) {
|
|
130
|
+
parts.push(...args.script_args);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return parts.join(' ');
|
|
134
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ScheduledRunArgs,
|
|
3
|
+
ScheduledUvArgs,
|
|
4
|
+
ScheduledPsArgs,
|
|
5
|
+
ScheduledJobArgs,
|
|
6
|
+
ScheduledJobSpec,
|
|
7
|
+
} from '../types.js';
|
|
8
|
+
import type { JobsApiClient } from '../api-client.js';
|
|
9
|
+
import { formatScheduledJobsTable, formatScheduledJobDetails } from '../formatters.js';
|
|
10
|
+
import { createJobSpec } from './utils.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Execute 'scheduled run' command
|
|
14
|
+
* Creates a scheduled job
|
|
15
|
+
*/
|
|
16
|
+
export async function scheduledRunCommand(args: ScheduledRunArgs, client: JobsApiClient): Promise<string> {
|
|
17
|
+
// Create job spec
|
|
18
|
+
const jobSpec = createJobSpec({
|
|
19
|
+
image: args.image,
|
|
20
|
+
command: args.command,
|
|
21
|
+
flavor: args.flavor,
|
|
22
|
+
env: args.env,
|
|
23
|
+
secrets: args.secrets,
|
|
24
|
+
timeout: args.timeout,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Create scheduled job spec
|
|
28
|
+
const scheduledSpec: ScheduledJobSpec = {
|
|
29
|
+
schedule: args.schedule,
|
|
30
|
+
suspend: args.suspend,
|
|
31
|
+
jobSpec,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Submit scheduled job
|
|
35
|
+
const scheduledJob = await client.createScheduledJob(scheduledSpec, args.namespace);
|
|
36
|
+
|
|
37
|
+
return `✓ Scheduled job created successfully!
|
|
38
|
+
|
|
39
|
+
**Scheduled Job ID:** ${scheduledJob.id}
|
|
40
|
+
**Schedule:** ${scheduledJob.schedule}
|
|
41
|
+
**Suspended:** ${scheduledJob.suspend ? 'Yes' : 'No'}
|
|
42
|
+
**Next Run:** ${scheduledJob.nextRun || 'N/A'}
|
|
43
|
+
|
|
44
|
+
To inspect: \`hf_jobs("scheduled inspect", {"scheduled_job_id": "${scheduledJob.id}"})\`
|
|
45
|
+
To list all: \`hf_jobs("scheduled ps")\``;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Execute 'scheduled uv' command
|
|
50
|
+
* Creates a scheduled UV job
|
|
51
|
+
*/
|
|
52
|
+
export async function scheduledUvCommand(args: ScheduledUvArgs, client: JobsApiClient): Promise<string> {
|
|
53
|
+
// For UV, use standard UV image
|
|
54
|
+
const image = 'ghcr.io/astral-sh/uv:python3.12-bookworm';
|
|
55
|
+
|
|
56
|
+
// Build UV command (similar to regular uv command)
|
|
57
|
+
const scriptSource = args.script;
|
|
58
|
+
let command: string;
|
|
59
|
+
|
|
60
|
+
if (scriptSource.startsWith('http://') || scriptSource.startsWith('https://')) {
|
|
61
|
+
command = buildUvCommand(scriptSource, args);
|
|
62
|
+
} else if (scriptSource.includes('\n')) {
|
|
63
|
+
const encoded = Buffer.from(scriptSource).toString('base64');
|
|
64
|
+
const deps =
|
|
65
|
+
args.with_deps && args.with_deps.length > 0 ? args.with_deps.map((dep) => `--with ${dep}`).join(' ') + ' ' : '';
|
|
66
|
+
command = `echo "${encoded}" | base64 -d | uv run ${deps}${args.python ? `-p ${args.python}` : ''} -`;
|
|
67
|
+
} else {
|
|
68
|
+
command = buildUvCommand(scriptSource, args);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Convert to scheduled run args
|
|
72
|
+
const scheduledRunArgs: ScheduledRunArgs = {
|
|
73
|
+
schedule: args.schedule,
|
|
74
|
+
suspend: args.suspend,
|
|
75
|
+
image,
|
|
76
|
+
command,
|
|
77
|
+
flavor: args.flavor,
|
|
78
|
+
env: args.env,
|
|
79
|
+
secrets: args.secrets,
|
|
80
|
+
timeout: args.timeout,
|
|
81
|
+
detach: args.detach,
|
|
82
|
+
namespace: args.namespace,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return scheduledRunCommand(scheduledRunArgs, client);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build UV command with options
|
|
90
|
+
*/
|
|
91
|
+
function buildUvCommand(
|
|
92
|
+
script: string,
|
|
93
|
+
args: { with_deps?: string[]; python?: string; script_args?: string[] }
|
|
94
|
+
): string {
|
|
95
|
+
const parts: string[] = ['uv', 'run'];
|
|
96
|
+
|
|
97
|
+
if (args.with_deps && args.with_deps.length > 0) {
|
|
98
|
+
for (const dep of args.with_deps) {
|
|
99
|
+
parts.push('--with', dep);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (args.python) {
|
|
104
|
+
parts.push('-p', args.python);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
parts.push(script);
|
|
108
|
+
|
|
109
|
+
if (args.script_args && args.script_args.length > 0) {
|
|
110
|
+
parts.push(...args.script_args);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return parts.join(' ');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Execute 'scheduled ps' command
|
|
118
|
+
* Lists scheduled jobs
|
|
119
|
+
*/
|
|
120
|
+
export async function scheduledPsCommand(args: ScheduledPsArgs, client: JobsApiClient): Promise<string> {
|
|
121
|
+
// Fetch all scheduled jobs
|
|
122
|
+
const allJobs = await client.listScheduledJobs(args.namespace);
|
|
123
|
+
|
|
124
|
+
// Filter jobs
|
|
125
|
+
let jobs = allJobs;
|
|
126
|
+
|
|
127
|
+
// Default: hide suspended jobs unless --all is specified
|
|
128
|
+
if (!args.all) {
|
|
129
|
+
jobs = jobs.filter((job) => !job.suspend);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Format as markdown table
|
|
133
|
+
const table = formatScheduledJobsTable(jobs);
|
|
134
|
+
|
|
135
|
+
if (jobs.length === 0) {
|
|
136
|
+
if (args.all) {
|
|
137
|
+
return 'No scheduled jobs found.';
|
|
138
|
+
}
|
|
139
|
+
return 'No active scheduled jobs found. Use `{"all": true}` to show suspended jobs.';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return `**Scheduled Jobs (${jobs.length} of ${allJobs.length} total):**
|
|
143
|
+
|
|
144
|
+
${table}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Execute 'scheduled inspect' command
|
|
149
|
+
* Gets details of a scheduled job
|
|
150
|
+
*/
|
|
151
|
+
export async function scheduledInspectCommand(args: ScheduledJobArgs, client: JobsApiClient): Promise<string> {
|
|
152
|
+
const job = await client.getScheduledJob(args.scheduled_job_id, args.namespace);
|
|
153
|
+
const formattedDetails = formatScheduledJobDetails(job);
|
|
154
|
+
return `**Scheduled Job Details:**\n\n${formattedDetails}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Execute 'scheduled delete' command
|
|
159
|
+
* Deletes a scheduled job
|
|
160
|
+
*/
|
|
161
|
+
export async function scheduledDeleteCommand(args: ScheduledJobArgs, client: JobsApiClient): Promise<string> {
|
|
162
|
+
await client.deleteScheduledJob(args.scheduled_job_id, args.namespace);
|
|
163
|
+
|
|
164
|
+
return `✓ Scheduled job ${args.scheduled_job_id} has been deleted.`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Execute 'scheduled suspend' command
|
|
169
|
+
* Suspends a scheduled job
|
|
170
|
+
*/
|
|
171
|
+
export async function scheduledSuspendCommand(args: ScheduledJobArgs, client: JobsApiClient): Promise<string> {
|
|
172
|
+
await client.suspendScheduledJob(args.scheduled_job_id, args.namespace);
|
|
173
|
+
|
|
174
|
+
return `✓ Scheduled job ${args.scheduled_job_id} has been suspended.
|
|
175
|
+
|
|
176
|
+
To resume: \`hf_jobs("scheduled resume", {"scheduled_job_id": "${args.scheduled_job_id}"})\``;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Execute 'scheduled resume' command
|
|
181
|
+
* Resumes a suspended scheduled job
|
|
182
|
+
*/
|
|
183
|
+
export async function scheduledResumeCommand(args: ScheduledJobArgs, client: JobsApiClient): Promise<string> {
|
|
184
|
+
await client.resumeScheduledJob(args.scheduled_job_id, args.namespace);
|
|
185
|
+
|
|
186
|
+
return `✓ Scheduled job ${args.scheduled_job_id} has been resumed.
|
|
187
|
+
|
|
188
|
+
To inspect: \`hf_jobs("scheduled inspect", {"scheduled_job_id": "${args.scheduled_job_id}"})\``;
|
|
189
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { JobSpec } from '../types.js';
|
|
2
|
+
import { parse as parseShellArgs } from 'shell-quote';
|
|
3
|
+
|
|
4
|
+
interface EnvToken {
|
|
5
|
+
type: 'env';
|
|
6
|
+
key: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const SPECIAL_PARAMS = new Set(['*', '@', '#', '?', '!', '-', '_']);
|
|
10
|
+
|
|
11
|
+
function isEnvToken(entry: unknown): entry is EnvToken {
|
|
12
|
+
return Boolean(entry && typeof entry === 'object' && (entry as EnvToken).type === 'env');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatEnvReference(key: string): string {
|
|
16
|
+
if (key === '') {
|
|
17
|
+
return '$';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (key === '$') {
|
|
21
|
+
return '$$';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (/^[A-Za-z0-9_]+$/.test(key)) {
|
|
25
|
+
return `$${key}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (SPECIAL_PARAMS.has(key)) {
|
|
29
|
+
return `$${key}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return `\${${key}}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse timeout string (e.g., "5m", "2h", "30s") to seconds
|
|
37
|
+
*/
|
|
38
|
+
export function parseTimeout(timeout: string): number {
|
|
39
|
+
const timeUnits: Record<'s' | 'm' | 'h' | 'd', number> = {
|
|
40
|
+
s: 1,
|
|
41
|
+
m: 60,
|
|
42
|
+
h: 3600,
|
|
43
|
+
d: 86400,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const match = timeout.match(/^(\d+(?:\.\d+)?)(s|m|h|d)$/);
|
|
47
|
+
if (!match || !match[1] || !match[2]) {
|
|
48
|
+
// Try to parse as plain number (seconds)
|
|
49
|
+
const seconds = parseInt(timeout, 10);
|
|
50
|
+
if (!isNaN(seconds)) {
|
|
51
|
+
return seconds;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Invalid timeout format: "${timeout}". Use format like "5m", "2h", "30s", or plain seconds.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const value = parseFloat(match[1]);
|
|
59
|
+
const unit = match[2] as 's' | 'm' | 'h' | 'd';
|
|
60
|
+
return Math.floor(value * timeUnits[unit]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Detect if image is a Space URL and extract spaceId
|
|
65
|
+
* Returns { dockerImage } or { spaceId }
|
|
66
|
+
*/
|
|
67
|
+
export function parseImageSource(image: string): { dockerImage?: string; spaceId?: string } {
|
|
68
|
+
const spacePrefixes = [
|
|
69
|
+
'https://huggingface.co/spaces/',
|
|
70
|
+
'https://hf.co/spaces/',
|
|
71
|
+
'huggingface.co/spaces/',
|
|
72
|
+
'hf.co/spaces/',
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
for (const prefix of spacePrefixes) {
|
|
76
|
+
if (image.startsWith(prefix)) {
|
|
77
|
+
return { spaceId: image.substring(prefix.length) };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Not a space, treat as docker image
|
|
82
|
+
return { dockerImage: image };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse command string or array into command array
|
|
87
|
+
* Uses shell-quote library for proper POSIX-compliant parsing
|
|
88
|
+
*/
|
|
89
|
+
export function parseCommand(command: string | string[]): { command: string[]; arguments?: string[] } {
|
|
90
|
+
// If already an array, return as-is
|
|
91
|
+
if (Array.isArray(command)) {
|
|
92
|
+
return { command, arguments: [] };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Parse the command string using shell-quote for POSIX-compliant parsing
|
|
96
|
+
const parsed = parseShellArgs<EnvToken>(command, key => ({ type: 'env', key }));
|
|
97
|
+
|
|
98
|
+
// Convert parsed result to string array
|
|
99
|
+
// shell-quote can return various types (strings, objects for operators, etc.)
|
|
100
|
+
// We filter to only keep string arguments
|
|
101
|
+
const stringArgs: string[] = [];
|
|
102
|
+
for (const arg of parsed) {
|
|
103
|
+
if (typeof arg === 'string') {
|
|
104
|
+
stringArgs.push(arg);
|
|
105
|
+
} else if (isEnvToken(arg)) {
|
|
106
|
+
stringArgs.push(formatEnvReference(arg.key));
|
|
107
|
+
} else {
|
|
108
|
+
// If we encounter a non-string (like operators), throw an error
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Unsupported shell syntax in command: "${command}". ` +
|
|
111
|
+
`Please use an array format for commands with complex shell operators, ` +
|
|
112
|
+
`or use simple quoted strings.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (stringArgs.length === 0) {
|
|
118
|
+
throw new Error(`Invalid command: "${command}". Command cannot be empty.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { command: stringArgs, arguments: [] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create a JobSpec from run command arguments
|
|
126
|
+
*/
|
|
127
|
+
export function createJobSpec(args: {
|
|
128
|
+
image: string;
|
|
129
|
+
command: string | string[];
|
|
130
|
+
flavor?: string;
|
|
131
|
+
env?: Record<string, string>;
|
|
132
|
+
secrets?: Record<string, string>;
|
|
133
|
+
timeout?: string;
|
|
134
|
+
}): JobSpec {
|
|
135
|
+
// Validate required fields
|
|
136
|
+
if (!args.image) {
|
|
137
|
+
throw new Error('image parameter is required. Provide a Docker image (e.g., "python:3.12") or Space URL.');
|
|
138
|
+
}
|
|
139
|
+
if (!args.command) {
|
|
140
|
+
throw new Error('command parameter is required. Provide a command as string or array.');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const imageSource = parseImageSource(args.image);
|
|
144
|
+
const { command, arguments: cmdArgs } = parseCommand(args.command);
|
|
145
|
+
const timeoutSeconds = args.timeout ? parseTimeout(args.timeout) : undefined;
|
|
146
|
+
|
|
147
|
+
const spec: JobSpec = {
|
|
148
|
+
...imageSource,
|
|
149
|
+
command,
|
|
150
|
+
arguments: cmdArgs,
|
|
151
|
+
flavor: args.flavor || 'cpu-basic',
|
|
152
|
+
environment: args.env || {}, // API requires object, not undefined
|
|
153
|
+
secrets: args.secrets || {}, // Same for secrets
|
|
154
|
+
timeoutSeconds,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return spec;
|
|
158
|
+
}
|