@minato-aqukin/autodl-cli 0.1.0

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.
@@ -0,0 +1,1458 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __nodeCreateRequire } from "node:module";
3
+ const require = __nodeCreateRequire(import.meta.url);
4
+ import {
5
+ BASE_IMAGES,
6
+ DEFAULT_BASE_IMAGE,
7
+ GPU_SPECS,
8
+ REGIONS,
9
+ VERSION,
10
+ assertProCreateRegion,
11
+ chooseRegions,
12
+ findBaseImage,
13
+ getBalance,
14
+ getStockByRegion,
15
+ parseCudaVersion,
16
+ resolveGpuSpec,
17
+ resolveRegion
18
+ } from "./chunk-CWCG3MOC.js";
19
+ import {
20
+ createContext
21
+ } from "./chunk-NBWWTYSL.js";
22
+ import {
23
+ AutoDLError,
24
+ BudgetError,
25
+ SSHError,
26
+ UsageError,
27
+ armTTLOverSSH,
28
+ composeStartCommand,
29
+ createInstance,
30
+ debug,
31
+ execCommand,
32
+ findInstance,
33
+ formatDuration,
34
+ formatYuan,
35
+ getCredentials,
36
+ getInstanceSnapshot,
37
+ getInstanceStatus,
38
+ isJson,
39
+ listAllInstances,
40
+ normalizeImage,
41
+ normalizePagination,
42
+ note,
43
+ parseDuration,
44
+ powerOffInstance,
45
+ powerOnInstance,
46
+ readConfig,
47
+ recordTTL,
48
+ redactSnapshot,
49
+ releaseInstance,
50
+ success,
51
+ sweepExpired,
52
+ t,
53
+ toAutoDLError,
54
+ untrackInstance,
55
+ waitForRunning,
56
+ waitForShutdown,
57
+ warn,
58
+ withSSH
59
+ } from "./chunk-6QR4ZYQR.js";
60
+
61
+ // src/core/endpoints/image.ts
62
+ async function saveImage(client, instanceUuid, imageName) {
63
+ const data = await client.post(
64
+ "/api/v1/dev/instance/pro/image/save",
65
+ { instance_uuid: instanceUuid, image_name: imageName },
66
+ { maxRetries: 0 }
67
+ );
68
+ return data.image_uuid;
69
+ }
70
+ async function listPrivateImages(client, options = {}) {
71
+ const data = await client.post(
72
+ "/api/v1/dev/instance/pro/image/private/list",
73
+ { page_index: options.pageIndex ?? 1, page_size: options.pageSize ?? 50 }
74
+ );
75
+ const list = Array.isArray(data?.list) ? data.list : [];
76
+ return { images: list.map(normalizeImage), pagination: normalizePagination(data) };
77
+ }
78
+
79
+ // src/core/repo.ts
80
+ var ACCELERATED_HOSTS = /* @__PURE__ */ new Set([
81
+ "github.com",
82
+ "raw.githubusercontent.com",
83
+ "githubusercontent.com",
84
+ "githubassets.com",
85
+ "huggingface.co"
86
+ ]);
87
+ var SCP_LIKE = /^(?:([^@]+)@)?([^:/]+):(.+)$/;
88
+ function parseRepo(input) {
89
+ const trimmed = input.trim();
90
+ if (!trimmed) throw new UsageError("\u4ED3\u5E93\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A");
91
+ let host;
92
+ let path;
93
+ if (/^https?:\/\//i.test(trimmed)) {
94
+ let url;
95
+ try {
96
+ url = new URL(trimmed);
97
+ } catch {
98
+ throw new UsageError(`\u65E0\u6CD5\u89E3\u6790\u4ED3\u5E93\u5730\u5740 "${input}"`);
99
+ }
100
+ host = url.hostname;
101
+ path = url.pathname;
102
+ } else if (trimmed.startsWith("git@") || SCP_LIKE.test(trimmed)) {
103
+ const match = SCP_LIKE.exec(trimmed);
104
+ if (!match) throw new UsageError(`\u65E0\u6CD5\u89E3\u6790\u4ED3\u5E93\u5730\u5740 "${input}"`);
105
+ host = match[2];
106
+ path = match[3];
107
+ } else if (/^[\w.-]+\/[\w.-]+$/.test(trimmed)) {
108
+ host = "github.com";
109
+ path = trimmed;
110
+ } else {
111
+ throw new UsageError(`\u65E0\u6CD5\u89E3\u6790\u4ED3\u5E93\u5730\u5740 "${input}"`, {
112
+ hint: "\u652F\u6301 https://github.com/owner/repo\u3001git@github.com:owner/repo.git \u6216 owner/repo\u3002"
113
+ });
114
+ }
115
+ path = path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
116
+ if (!path?.includes("/")) {
117
+ throw new UsageError(`\u4ED3\u5E93\u5730\u5740\u7F3A\u5C11 owner/name\uFF1A"${input}"`);
118
+ }
119
+ const name = path.split("/").pop();
120
+ return {
121
+ host,
122
+ path,
123
+ name,
124
+ cloneUrl: `https://${host}/${path}.git`,
125
+ needsAcceleration: ACCELERATED_HOSTS.has(host)
126
+ };
127
+ }
128
+ function withCredentials(repo, token) {
129
+ if (!token) return { url: repo.cloneUrl, display: repo.cloneUrl };
130
+ return {
131
+ url: `https://x-access-token:${token}@${repo.host}/${repo.path}.git`,
132
+ display: `https://x-access-token:***@${repo.host}/${repo.path}.git`
133
+ };
134
+ }
135
+ function redactCredentials(text) {
136
+ return text.replace(/(https?:\/\/)([^/@\s]+)@/gi, "$1***@");
137
+ }
138
+ function resolveGitToken(explicit) {
139
+ const candidate = explicit ?? process.env.GIT_TOKEN ?? process.env.GITHUB_TOKEN ?? process.env.GITEE_TOKEN;
140
+ return candidate?.trim() ? candidate.trim() : void 0;
141
+ }
142
+
143
+ // src/guard/budget.ts
144
+ var DEFAULT_MIN_BALANCE_YUAN = 5;
145
+ function resolveMinBalance(explicit) {
146
+ if (explicit !== void 0) return explicit;
147
+ const fromEnv = Number(process.env.AUTODL_MIN_BALANCE);
148
+ if (!Number.isNaN(fromEnv) && process.env.AUTODL_MIN_BALANCE) return fromEnv;
149
+ return readConfig().defaults?.minBalanceYuan ?? DEFAULT_MIN_BALANCE_YUAN;
150
+ }
151
+ async function assertBudget(client, minYuan) {
152
+ const threshold = resolveMinBalance(minYuan);
153
+ if (threshold <= 0) {
154
+ debug("\u4F59\u989D\u95F8\u95E8\u5DF2\u7981\u7528\uFF08\u9608\u503C <= 0\uFF09");
155
+ return Number.POSITIVE_INFINITY;
156
+ }
157
+ const balance = await getBalance(client);
158
+ const spendable = balance.balanceYuan + balance.voucherYuan;
159
+ debug(`\u4F59\u989D\u68C0\u67E5\uFF1A\u53EF\u7528 ${formatYuan(spendable)}\uFF0C\u9608\u503C ${formatYuan(threshold)}`);
160
+ if (spendable < threshold) {
161
+ throw new BudgetError(
162
+ `\u8D26\u53F7\u53EF\u7528\u4F59\u989D ${formatYuan(spendable)} \u4F4E\u4E8E\u9608\u503C ${formatYuan(threshold)}\uFF0C\u5DF2\u963B\u6B62\u521B\u5EFA\u5B9E\u4F8B`,
163
+ {
164
+ code: "INSUFFICIENT_BALANCE",
165
+ hint: "\u5148\u5145\u503C\uFF0C\u6216\u7528 --min-balance 0 \u663E\u5F0F\u8DF3\u8FC7\u8FD9\u9053\u95F8\u95E8\u3002",
166
+ details: { spendableYuan: spendable, thresholdYuan: threshold }
167
+ }
168
+ );
169
+ }
170
+ return spendable;
171
+ }
172
+
173
+ // src/guard/idle.ts
174
+ var QUERY = "nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits";
175
+ function parseUtilisation(stdout) {
176
+ const values = stdout.split("\n").map((line) => line.trim()).filter(Boolean).map(Number).filter((n) => !Number.isNaN(n));
177
+ if (values.length === 0) return null;
178
+ return values.reduce((sum, v) => sum + v, 0) / values.length;
179
+ }
180
+ function sleep(ms, signal) {
181
+ return new Promise((resolve) => {
182
+ const timer = setTimeout(resolve, ms);
183
+ signal?.addEventListener(
184
+ "abort",
185
+ () => {
186
+ clearTimeout(timer);
187
+ resolve();
188
+ },
189
+ { once: true }
190
+ );
191
+ });
192
+ }
193
+ async function watchIdle(client, uuid, options = {}) {
194
+ const threshold = options.thresholdPercent ?? 5;
195
+ const required = options.samples ?? 6;
196
+ const intervalSeconds = options.intervalSeconds ?? 60;
197
+ let consecutive = 0;
198
+ let taken = 0;
199
+ let last = null;
200
+ note(
201
+ `\u95F2\u7F6E\u68C0\u6D4B\u5DF2\u542F\u52A8\uFF1AGPU \u5229\u7528\u7387 \u2264 ${threshold}% \u8FDE\u7EED ${required} \u6B21\uFF08\u6BCF ${formatDuration(intervalSeconds)} \u4E00\u6B21\uFF09\u540E\u81EA\u52A8\u5173\u673A`
202
+ );
203
+ while (!options.signal?.aborted) {
204
+ let utilisation = null;
205
+ try {
206
+ const result = await execCommand(client, uuid, QUERY, { capture: true, timeoutMs: 6e4 });
207
+ utilisation = parseUtilisation(result.stdout);
208
+ } catch (err) {
209
+ debug(`\u91C7\u6837\u5931\u8D25\uFF0C\u8DF3\u8FC7\u672C\u6B21\uFF1A${err.message}`);
210
+ await sleep(intervalSeconds * 1e3, options.signal);
211
+ continue;
212
+ }
213
+ taken++;
214
+ if (utilisation === null) {
215
+ warn("nvidia-smi \u672A\u8FD4\u56DE\u53EF\u7528\u6570\u636E\uFF0C\u8DF3\u8FC7\u672C\u6B21\u91C7\u6837");
216
+ await sleep(intervalSeconds * 1e3, options.signal);
217
+ continue;
218
+ }
219
+ last = utilisation;
220
+ consecutive = utilisation <= threshold ? consecutive + 1 : 0;
221
+ options.onSample?.(utilisation, consecutive);
222
+ debug(`GPU \u5229\u7528\u7387 ${utilisation.toFixed(1)}%\uFF08\u8FDE\u7EED\u95F2\u7F6E ${consecutive}/${required}\uFF09`);
223
+ if (consecutive >= required) {
224
+ if (options.dryRun) {
225
+ note("dry-run\uFF1A\u672C\u5E94\u5728\u6B64\u5173\u673A");
226
+ return { stopped: false, samplesTaken: taken, lastUtilisation: last, reason: "dry-run" };
227
+ }
228
+ await powerOffInstance(client, uuid);
229
+ return { stopped: true, samplesTaken: taken, lastUtilisation: last, reason: "idle" };
230
+ }
231
+ await sleep(intervalSeconds * 1e3, options.signal);
232
+ }
233
+ return { stopped: false, samplesTaken: taken, lastUtilisation: last, reason: "aborted" };
234
+ }
235
+
236
+ // src/ssh/transfer.ts
237
+ import { createReadStream, createWriteStream } from "fs";
238
+ import { mkdir, readdir, readFile, stat } from "fs/promises";
239
+ import { basename, dirname, join, posix, relative, resolve as resolvePath, sep } from "path";
240
+ import { pipeline } from "stream/promises";
241
+ var ALWAYS_IGNORE = [
242
+ ".git",
243
+ "node_modules",
244
+ "__pycache__",
245
+ ".venv",
246
+ "venv",
247
+ ".mypy_cache",
248
+ ".pytest_cache",
249
+ ".DS_Store",
250
+ "*.pyc"
251
+ ];
252
+ function sftp(conn) {
253
+ return new Promise((resolve, reject) => {
254
+ conn.sftp((err, handle) => {
255
+ if (err) reject(new SSHError(`SFTP \u901A\u9053\u5EFA\u7ACB\u5931\u8D25\uFF1A${err.message}`, { cause: err }));
256
+ else resolve(handle);
257
+ });
258
+ });
259
+ }
260
+ function toMatcher(pattern) {
261
+ const trimmed = pattern.trim();
262
+ const anchored = trimmed.startsWith("/");
263
+ const body = (anchored ? trimmed.slice(1) : trimmed).replace(/\/$/, "");
264
+ const escaped = body.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
265
+ const regex = new RegExp(anchored ? `^${escaped}(/|$)` : `(^|/)${escaped}(/|$)`);
266
+ return (relPath) => regex.test(relPath);
267
+ }
268
+ async function loadIgnoreFile(root, name) {
269
+ try {
270
+ const content = await readFile(join(root, name), "utf8");
271
+ return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && !line.startsWith("!"));
272
+ } catch {
273
+ return [];
274
+ }
275
+ }
276
+ async function buildIgnoreMatcher(root, options) {
277
+ const patterns = [...ALWAYS_IGNORE, ...options.ignore ?? []];
278
+ if (!options.noIgnoreFiles) {
279
+ const fromAutodl = await loadIgnoreFile(root, ".autodlignore");
280
+ patterns.push(...fromAutodl.length ? fromAutodl : await loadIgnoreFile(root, ".gitignore"));
281
+ }
282
+ const matchers = patterns.map(toMatcher);
283
+ return (relPath) => matchers.some((match) => match(relPath));
284
+ }
285
+ async function collectFiles(root, ignored) {
286
+ const files = [];
287
+ async function walk(dir) {
288
+ const entries = await readdir(dir, { withFileTypes: true });
289
+ for (const entry of entries) {
290
+ const absolute = join(dir, entry.name);
291
+ const rel = relative(root, absolute).split(sep).join("/");
292
+ if (ignored(rel)) {
293
+ debug(`\u8DF3\u8FC7 ${rel}`);
294
+ continue;
295
+ }
296
+ if (entry.isDirectory()) await walk(absolute);
297
+ else if (entry.isFile()) files.push(rel);
298
+ }
299
+ }
300
+ await walk(root);
301
+ return files;
302
+ }
303
+ function mkdirRemote(handle, path) {
304
+ return new Promise((resolve) => {
305
+ handle.mkdir(path, () => resolve());
306
+ });
307
+ }
308
+ async function ensureRemoteDir(handle, path) {
309
+ const segments = path.split("/").filter(Boolean);
310
+ let current = path.startsWith("/") ? "" : ".";
311
+ for (const segment of segments) {
312
+ current = `${current}/${segment}`;
313
+ await mkdirRemote(handle, current);
314
+ }
315
+ }
316
+ function uploadFile(handle, local, remote) {
317
+ return pipeline(createReadStream(local), handle.createWriteStream(remote));
318
+ }
319
+ function downloadFile(handle, remote, local) {
320
+ return pipeline(handle.createReadStream(remote), createWriteStream(local));
321
+ }
322
+ function statRemote(handle, path) {
323
+ return new Promise((resolve, reject) => {
324
+ handle.stat(path, (err, stats) => {
325
+ if (err) reject(new AutoDLError(`\u8FDC\u7A0B\u8DEF\u5F84\u4E0D\u53EF\u8BBF\u95EE\uFF1A${path}`, { cause: err }));
326
+ else resolve({ isDirectory: stats.isDirectory(), size: stats.size });
327
+ });
328
+ });
329
+ }
330
+ function readdirRemote(handle, path) {
331
+ return new Promise((resolve, reject) => {
332
+ handle.readdir(path, (err, list) => {
333
+ if (err) reject(new AutoDLError(`\u65E0\u6CD5\u5217\u51FA\u8FDC\u7A0B\u76EE\u5F55\uFF1A${path}`, { cause: err }));
334
+ else
335
+ resolve(
336
+ list.map((item) => ({
337
+ name: item.filename,
338
+ isDirectory: (item.attrs.mode & 61440) === 16384
339
+ }))
340
+ );
341
+ });
342
+ });
343
+ }
344
+ async function push(client, uuid, localPath, remotePath, options = {}) {
345
+ const local = resolvePath(localPath);
346
+ const info = await stat(local).catch(() => null);
347
+ if (!info) throw new AutoDLError(`\u672C\u5730\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A${localPath}`);
348
+ return withSSH(
349
+ client,
350
+ uuid,
351
+ async (conn) => {
352
+ const handle = await sftp(conn);
353
+ try {
354
+ if (info.isFile()) {
355
+ const target = remotePath.endsWith("/") ? posix.join(remotePath, basename(local)) : remotePath;
356
+ await ensureRemoteDir(handle, posix.dirname(target));
357
+ await uploadFile(handle, local, target);
358
+ options.onProgress?.({ file: basename(local), index: 1, total: 1, bytes: info.size });
359
+ return { files: 1, bytes: info.size };
360
+ }
361
+ const ignored = await buildIgnoreMatcher(local, options);
362
+ const files = await collectFiles(local, ignored);
363
+ await ensureRemoteDir(handle, remotePath);
364
+ let bytes = 0;
365
+ const dirs = new Set(files.map((f) => posix.dirname(f)).filter((d) => d !== "."));
366
+ for (const dir of [...dirs].sort()) {
367
+ await ensureRemoteDir(handle, posix.join(remotePath, dir));
368
+ }
369
+ for (const [index, rel] of files.entries()) {
370
+ const source = join(local, rel);
371
+ const size = (await stat(source)).size;
372
+ await uploadFile(handle, source, posix.join(remotePath, rel));
373
+ bytes += size;
374
+ options.onProgress?.({ file: rel, index: index + 1, total: files.length, bytes: size });
375
+ }
376
+ return { files: files.length, bytes };
377
+ } finally {
378
+ handle.end();
379
+ }
380
+ },
381
+ options
382
+ );
383
+ }
384
+ async function pull(client, uuid, remotePath, localPath, options = {}) {
385
+ const local = resolvePath(localPath);
386
+ return withSSH(
387
+ client,
388
+ uuid,
389
+ async (conn) => {
390
+ const handle = await sftp(conn);
391
+ try {
392
+ const info = await statRemote(handle, remotePath);
393
+ if (!info.isDirectory) {
394
+ const target = local.endsWith(sep) ? join(local, basename(remotePath)) : local;
395
+ await mkdir(dirname(target), { recursive: true });
396
+ await downloadFile(handle, remotePath, target);
397
+ options.onProgress?.({
398
+ file: basename(remotePath),
399
+ index: 1,
400
+ total: 1,
401
+ bytes: info.size
402
+ });
403
+ return { files: 1, bytes: info.size };
404
+ }
405
+ const collected = [];
406
+ const walk = async (dir, prefix) => {
407
+ for (const entry of await readdirRemote(handle, dir)) {
408
+ const remote = posix.join(dir, entry.name);
409
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
410
+ if (entry.isDirectory) await walk(remote, rel);
411
+ else collected.push({ remote, rel, size: (await statRemote(handle, remote)).size });
412
+ }
413
+ };
414
+ await walk(remotePath, "");
415
+ let bytes = 0;
416
+ for (const [index, item] of collected.entries()) {
417
+ const target = join(local, item.rel);
418
+ await mkdir(dirname(target), { recursive: true });
419
+ await downloadFile(handle, item.remote, target);
420
+ bytes += item.size;
421
+ options.onProgress?.({
422
+ file: item.rel,
423
+ index: index + 1,
424
+ total: collected.length,
425
+ bytes: item.size
426
+ });
427
+ }
428
+ return { files: collected.length, bytes };
429
+ } finally {
430
+ handle.end();
431
+ }
432
+ },
433
+ options
434
+ );
435
+ }
436
+
437
+ // src/workflow/deploy.ts
438
+ var DATA_DISK = "/root/autodl-tmp";
439
+ var ALIVE_MARKER = "AUTODL_START_OK";
440
+ var DEAD_MARKER = "AUTODL_START_DEAD";
441
+ var SETUP_RECIPES = [
442
+ { file: "environment.yml", command: "conda env update -f environment.yml --prune" },
443
+ { file: "requirements.txt", command: "pip install -r requirements.txt" },
444
+ { file: "pyproject.toml", command: "pip install -e ." },
445
+ { file: "package-lock.json", command: "npm ci" },
446
+ { file: "package.json", command: "npm install" }
447
+ ];
448
+ function quote(value) {
449
+ return `'${value.replace(/'/g, `'\\''`)}'`;
450
+ }
451
+ function withAcceleration(command, enabled) {
452
+ return enabled ? `source /etc/network_turbo >/dev/null 2>&1; ${command}` : command;
453
+ }
454
+ async function run(client, uuid, command, options, { label, capture = true }) {
455
+ debug(`[${label}] ${redactCredentials(command)}`);
456
+ const result = await execCommand(client, uuid, command, {
457
+ capture,
458
+ stdout: isJson() ? process.stderr : process.stdout,
459
+ stderr: process.stderr,
460
+ ...options.commandTimeoutMs !== void 0 ? { timeoutMs: options.commandTimeoutMs } : {},
461
+ ...options.signal ? { signal: options.signal } : {}
462
+ });
463
+ return result;
464
+ }
465
+ async function detectSetup(client, uuid, dir, options) {
466
+ const probe = SETUP_RECIPES.map(
467
+ (recipe2) => `[ -f ${quote(`${dir}/${recipe2.file}`)} ] && echo ${quote(recipe2.file)}`
468
+ ).join("; ");
469
+ const result = await run(client, uuid, `${probe}; true`, options, {
470
+ label: "detect",
471
+ capture: true
472
+ });
473
+ const present = new Set(
474
+ result.stdout.split("\n").map((line) => line.trim()).filter(Boolean)
475
+ );
476
+ const recipe = SETUP_RECIPES.find((candidate) => present.has(candidate.file));
477
+ if (!recipe) {
478
+ note(
479
+ "\u672A\u63A2\u6D4B\u5230\u4F9D\u8D56\u6E05\u5355\uFF08environment.yml / requirements.txt / pyproject.toml / package.json\uFF09\uFF0C\u8DF3\u8FC7\u5B89\u88C5"
480
+ );
481
+ return null;
482
+ }
483
+ note(`\u63A2\u6D4B\u5230 ${recipe.file}\uFF0C\u5C06\u6267\u884C\uFF1A${recipe.command}`);
484
+ return recipe.command;
485
+ }
486
+ async function checkout(client, uuid, repo, dir, options) {
487
+ const accelerate = repo.needsAcceleration && !options.noAcceleration;
488
+ if (accelerate) note("\u5DF2\u542F\u7528\u5B66\u672F\u8D44\u6E90\u52A0\u901F\uFF08\u4EC5\u8986\u76D6 GitHub / HuggingFace\uFF09");
489
+ const { url } = withCredentials(repo, options.gitToken);
490
+ const branch = options.branch;
491
+ const update = [
492
+ `cd ${quote(dir)}`,
493
+ `git remote set-url origin ${quote(url)}`,
494
+ "git fetch --depth 1 origin",
495
+ branch ? `git checkout -B ${quote(branch)} origin/${quote(branch)}` : "git reset --hard @{u}"
496
+ ].join(" && ");
497
+ const clone = [
498
+ `mkdir -p ${quote(DATA_DISK)}`,
499
+ `git clone --depth 1 ${branch ? `--branch ${quote(branch)} ` : ""}${quote(url)} ${quote(dir)}`
500
+ ].join(" && ");
501
+ const scrub = `cd ${quote(dir)} && git remote set-url origin ${quote(repo.cloneUrl)}`;
502
+ const command = withAcceleration(
503
+ `if [ -d ${quote(`${dir}/.git`)} ]; then ${update}; else ${clone}; fi && ${scrub}`,
504
+ accelerate
505
+ );
506
+ const result = await run(client, uuid, command, options, { label: "checkout", capture: true });
507
+ if (result.exitCode !== 0) {
508
+ throw new UsageError(`\u62C9\u53D6\u4ED3\u5E93\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${result.exitCode}\uFF09`, {
509
+ hint: options.gitToken ? "\u786E\u8BA4 token \u5BF9\u8BE5\u4ED3\u5E93\u6709\u8BFB\u6743\u9650\u3002" : "\u79C1\u6709\u4ED3\u5E93\u9700\u8981 --git-token\uFF0C\u6216\u8BBE\u7F6E GIT_TOKEN / GITHUB_TOKEN \u73AF\u5883\u53D8\u91CF\u3002",
510
+ details: { stderr: redactCredentials(result.stderr).slice(0, 500) }
511
+ });
512
+ }
513
+ success(`\u4EE3\u7801\u5DF2\u5C31\u7EEA\uFF1A${dir}`);
514
+ }
515
+ async function describeAccess(client, uuid, dir, detached) {
516
+ if (!detached) return { publicUrls: [], tunnelHint: null, logHint: null };
517
+ const logHint = `autodl exec ${uuid} "tail -f ${dir}/.autodl/start.log"`;
518
+ try {
519
+ const snapshot = await getInstanceSnapshot(client, uuid);
520
+ const publicUrls = snapshot.services.map(
521
+ (service) => `${service.protocol === "http" ? "https://" : ""}${service.domain} (:${service.port})`
522
+ );
523
+ return {
524
+ publicUrls,
525
+ // 6006/6008 public mapping needs enterprise verification; personal accounts tunnel.
526
+ tunnelHint: publicUrls.length ? null : `autodl ssh ${uuid} -L 6006:localhost:6006`,
527
+ logHint
528
+ };
529
+ } catch {
530
+ return { publicUrls: [], tunnelHint: `autodl ssh ${uuid} -L 6006:localhost:6006`, logHint };
531
+ }
532
+ }
533
+ async function deployWorkflow(client, options) {
534
+ const startedAt = Date.now();
535
+ const repo = parseRepo(options.repo);
536
+ const dir = options.dir ?? `${DATA_DISK}/${repo.name}`;
537
+ const onFinish = options.onFinish ?? "poweroff";
538
+ let uuid = options.instanceUuid;
539
+ let created = false;
540
+ if (uuid) {
541
+ const status = await getInstanceStatus(client, uuid);
542
+ if (status !== "running") {
543
+ note(t("instance.poweringOn"));
544
+ await powerOnInstance(client, uuid, {
545
+ startCommand: composeStartCommand(options.ttlSeconds, void 0)
546
+ });
547
+ await waitForRunning(client, uuid, {
548
+ ...options.signal ? { signal: options.signal } : {}
549
+ });
550
+ }
551
+ const armed = await armTTLOverSSH(client, uuid, options.ttlSeconds);
552
+ if (!armed) warn(t("guard.armFailed"));
553
+ recordTTL({ uuid, ttlSeconds: options.ttlSeconds, inInstanceTimer: armed });
554
+ success(`\u590D\u7528\u5B9E\u4F8B ${uuid}`);
555
+ } else {
556
+ if (!options.gpu) {
557
+ throw new UsageError("\u5FC5\u987B\u6307\u5B9A --gpu\uFF08\u6216\u7528 --instance \u590D\u7528\u5DF2\u6709\u5B9E\u4F8B\uFF09", {
558
+ hint: "\u8FD0\u884C `autodl gpus` \u67E5\u770B\u89C4\u683C\uFF0C`autodl stock` \u67E5\u770B\u54EA\u91CC\u6709\u8D27\u3002"
559
+ });
560
+ }
561
+ const spec = resolveGpuSpec(options.gpu);
562
+ if (!spec) {
563
+ throw new UsageError(`\u672A\u77E5\u7684 GPU \u89C4\u683C "${options.gpu}"`, {
564
+ hint: "\u8FD0\u884C `autodl gpus` \u67E5\u770B\u5B98\u65B9 API \u652F\u6301\u7684\u5168\u90E8\u89C4\u683C\u3002"
565
+ });
566
+ }
567
+ const imageInput = options.image ?? DEFAULT_BASE_IMAGE;
568
+ const image = findBaseImage(imageInput);
569
+ const imageUuid = image?.uuid ?? imageInput;
570
+ const requestedRegions = (options.regions ?? []).map(
571
+ (input) => assertProCreateRegion(input).id
572
+ );
573
+ await assertBudget(client, options.minBalanceYuan);
574
+ const regions = options.stockCheck === false ? requestedRegions : (await chooseRegions(client, spec, requestedRegions)).regions;
575
+ note(t("instance.creating"));
576
+ uuid = await createInstance(client, {
577
+ gpuSpec: spec.id,
578
+ gpuNum: options.gpuNum ?? 1,
579
+ imageUuid,
580
+ cudaFrom: parseCudaVersion(image?.cuda ?? "11.8"),
581
+ expandSystemDiskGb: options.diskGb ?? 0,
582
+ ...regions.length ? { regions } : {},
583
+ name: options.name ?? repo.name,
584
+ ...composeStartCommand(options.ttlSeconds, void 0) ? { startCommand: composeStartCommand(options.ttlSeconds, void 0) } : {}
585
+ });
586
+ created = true;
587
+ recordTTL({
588
+ uuid,
589
+ name: options.name ?? repo.name,
590
+ ttlSeconds: options.ttlSeconds,
591
+ inInstanceTimer: true
592
+ });
593
+ success(`${t("instance.created")}\uFF1A${uuid}\uFF08TTL ${formatDuration(options.ttlSeconds)}\uFF09`);
594
+ note(t("instance.waiting"));
595
+ await waitForRunning(client, uuid, { ...options.signal ? { signal: options.signal } : {} });
596
+ success(t("instance.ready"));
597
+ }
598
+ const instanceUuid = uuid;
599
+ let setupCommand = null;
600
+ let startCommand = null;
601
+ let exitCode = null;
602
+ let stdout = "";
603
+ let stderr = "";
604
+ let access = { publicUrls: [], tunnelHint: null, logHint: null };
605
+ try {
606
+ await checkout(client, instanceUuid, repo, dir, options);
607
+ if (!options.noSetup) {
608
+ setupCommand = options.setup ?? await detectSetup(client, instanceUuid, dir, options);
609
+ if (setupCommand) {
610
+ note("\u6B63\u5728\u5B89\u88C5\u4F9D\u8D56\u2026");
611
+ const accelerate = repo.needsAcceleration && !options.noAcceleration;
612
+ const result = await run(
613
+ client,
614
+ instanceUuid,
615
+ withAcceleration(`cd ${quote(dir)} && ${setupCommand}`, accelerate),
616
+ options,
617
+ { label: "setup", capture: true }
618
+ );
619
+ if (result.exitCode !== 0) {
620
+ throw new UsageError(`\u4F9D\u8D56\u5B89\u88C5\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${result.exitCode}\uFF09`, {
621
+ hint: "\u53EF\u4EE5\u7528 --setup \u6307\u5B9A\u81EA\u5B9A\u4E49\u5B89\u88C5\u547D\u4EE4\uFF0C\u6216 --no-setup \u8DF3\u8FC7\u540E\u624B\u52A8\u5904\u7406\u3002"
622
+ });
623
+ }
624
+ success("\u4F9D\u8D56\u5B89\u88C5\u5B8C\u6210");
625
+ }
626
+ }
627
+ if (options.start) {
628
+ startCommand = options.start;
629
+ if (options.detach) {
630
+ const detached = [
631
+ `cd ${quote(dir)}`,
632
+ "mkdir -p .autodl",
633
+ `{ nohup ${options.start} > .autodl/start.log 2>&1 < /dev/null & echo $! > .autodl/start.pid; }`,
634
+ "sleep 2",
635
+ `if kill -0 "$(cat .autodl/start.pid)" 2>/dev/null; then echo "${ALIVE_MARKER} $(cat .autodl/start.pid)"; else echo "${DEAD_MARKER}"; tail -50 .autodl/start.log; exit 1; fi`
636
+ ].join(" && ");
637
+ const result = await run(client, instanceUuid, detached, options, {
638
+ label: "start",
639
+ capture: true
640
+ });
641
+ exitCode = result.exitCode;
642
+ stdout = result.stdout;
643
+ stderr = result.stderr;
644
+ if (result.exitCode !== 0 || result.stdout.includes(DEAD_MARKER)) {
645
+ throw new UsageError("\u540E\u53F0\u542F\u52A8\u5931\u8D25\uFF1A\u8FDB\u7A0B\u5DF2\u9000\u51FA", {
646
+ hint: `\u5B8C\u6574\u65E5\u5FD7\u5728\u5B9E\u4F8B\u4E0A\u7684 ${dir}/.autodl/start.log`,
647
+ details: {
648
+ log: result.stdout.replace(DEAD_MARKER, "").trim().slice(0, 800),
649
+ stderr: result.stderr.slice(0, 400)
650
+ }
651
+ });
652
+ }
653
+ success("\u5DF2\u5728\u540E\u53F0\u542F\u52A8\u5E76\u786E\u8BA4\u8FDB\u7A0B\u5B58\u6D3B");
654
+ } else {
655
+ note("\u6B63\u5728\u542F\u52A8\u9879\u76EE\u2026");
656
+ const result = await run(
657
+ client,
658
+ instanceUuid,
659
+ `cd ${quote(dir)} && ${options.start}`,
660
+ options,
661
+ { label: "start", capture: true }
662
+ );
663
+ exitCode = result.exitCode;
664
+ stdout = result.stdout;
665
+ stderr = result.stderr;
666
+ }
667
+ }
668
+ access = await describeAccess(
669
+ client,
670
+ instanceUuid,
671
+ dir,
672
+ Boolean(options.detach && options.start)
673
+ );
674
+ } finally {
675
+ await finish(client, instanceUuid, onFinish, Boolean(options.detach && options.start));
676
+ }
677
+ return {
678
+ instanceUuid,
679
+ created,
680
+ repo: { host: repo.host, path: repo.path, name: repo.name, branch: options.branch ?? null },
681
+ dir,
682
+ setupCommand,
683
+ startCommand,
684
+ detached: Boolean(options.detach && options.start),
685
+ exitCode,
686
+ stdout,
687
+ stderr,
688
+ access,
689
+ // A detached run deliberately leaves the instance up, so reporting the requested
690
+ // `poweroff` here would be a lie the caller might act on.
691
+ finalAction: options.detach && options.start ? "keep" : onFinish,
692
+ durationMs: Date.now() - startedAt
693
+ };
694
+ }
695
+ async function finish(client, uuid, action, detached) {
696
+ if (detached) {
697
+ warn(`\u5B9E\u4F8B ${uuid} \u4FDD\u6301\u8FD0\u884C\u4E2D\uFF08--detach\uFF09\uFF0C\u7528\u5B8C\u8BF7\u5173\u673A\uFF1Aautodl stop ${uuid}`);
698
+ return;
699
+ }
700
+ if (action === "keep") {
701
+ warn(`\u5B9E\u4F8B ${uuid} \u4ECD\u5728\u8FD0\u884C\uFF08--on-finish keep\uFF09\uFF0C\u8BB0\u5F97\u624B\u52A8\u5173\u673A\uFF1Aautodl stop ${uuid}`);
702
+ return;
703
+ }
704
+ note(t("run.cleanup"));
705
+ try {
706
+ const status = await getInstanceStatus(client, uuid).catch(() => "unknown");
707
+ if (status !== "shutdown" && status !== "shutting_down") {
708
+ await powerOffInstance(client, uuid);
709
+ }
710
+ success(`\u5B9E\u4F8B ${uuid} \u5DF2\u5173\u673A\uFF0C\u8BA1\u8D39\u5DF2\u505C\u6B62\uFF08\u6570\u636E\u4FDD\u7559\uFF0C\u53EF\u518D\u6B21\u90E8\u7F72\u590D\u7528\uFF09`);
711
+ } catch (err) {
712
+ warn(`\u81EA\u52A8\u5173\u673A\u5931\u8D25\uFF1A${err.message}`);
713
+ warn(`\u8BF7\u7ACB\u5373\u624B\u52A8\u5904\u7406\uFF1Aautodl stop ${uuid}`);
714
+ return;
715
+ }
716
+ if (action === "release") {
717
+ try {
718
+ await waitForShutdown(client, uuid, { timeoutMs: 10 * 6e4 });
719
+ await releaseInstance(client, uuid);
720
+ untrackInstance(uuid);
721
+ success(`\u5B9E\u4F8B ${uuid} \u5DF2\u91CA\u653E`);
722
+ } catch (err) {
723
+ warn(`\u91CA\u653E\u5931\u8D25\uFF08\u5B9E\u4F8B\u5DF2\u5173\u673A\uFF0C\u4E0D\u518D\u8BA1\u8D39\uFF09\uFF1A${err.message}`);
724
+ warn(`\u7A0D\u540E\u53EF\u91CD\u8BD5\uFF1Aautodl rm ${uuid} --yes`);
725
+ }
726
+ } else {
727
+ untrackInstance(uuid);
728
+ }
729
+ }
730
+
731
+ // src/workflow/run.ts
732
+ import { mkdir as mkdir2 } from "fs/promises";
733
+ var DEFAULT_WORKDIR = "/root/autodl-tmp/autodl-cli";
734
+ async function runWorkflow(client, options) {
735
+ const startedAt = Date.now();
736
+ const spec = resolveGpuSpec(options.gpu);
737
+ if (!spec) {
738
+ throw new UsageError(`\u672A\u77E5\u7684 GPU \u89C4\u683C "${options.gpu}"`, {
739
+ hint: "\u8FD0\u884C `autodl gpus` \u67E5\u770B\u5B98\u65B9 API \u652F\u6301\u7684\u5168\u90E8\u89C4\u683C\u3002"
740
+ });
741
+ }
742
+ const imageInput = options.image ?? DEFAULT_BASE_IMAGE;
743
+ const image = findBaseImage(imageInput);
744
+ const imageUuid = image?.uuid ?? imageInput;
745
+ const cudaFrom = options.cudaFrom ? parseCudaVersion(options.cudaFrom) : parseCudaVersion(image?.cuda ?? "11.8");
746
+ const requestedRegions = (options.regions ?? []).map((input) => assertProCreateRegion(input).id);
747
+ const workdir = options.workdir ?? DEFAULT_WORKDIR;
748
+ const onFinish = options.onFinish ?? "poweroff";
749
+ await assertBudget(client, options.minBalanceYuan);
750
+ const regions = options.stockCheck === false ? requestedRegions : (await chooseRegions(client, spec, requestedRegions)).regions;
751
+ const startCommand = composeStartCommand(options.ttlSeconds, void 0);
752
+ note(t("instance.creating"));
753
+ const uuid = await createInstance(client, {
754
+ gpuSpec: spec.id,
755
+ gpuNum: options.gpuNum ?? 1,
756
+ imageUuid,
757
+ cudaFrom,
758
+ ...options.diskGb !== void 0 ? { expandSystemDiskGb: options.diskGb } : {},
759
+ ...regions.length ? { regions } : {},
760
+ ...options.name ? { name: options.name } : {},
761
+ ...startCommand ? { startCommand } : {}
762
+ });
763
+ recordTTL({
764
+ uuid,
765
+ ...options.name ? { name: options.name } : {},
766
+ ttlSeconds: options.ttlSeconds,
767
+ inInstanceTimer: true
768
+ });
769
+ success(`${t("instance.created")}\uFF1A${uuid}\uFF08TTL ${formatDuration(options.ttlSeconds)}\uFF09`);
770
+ let uploaded = null;
771
+ let downloaded = null;
772
+ let exitCode = null;
773
+ let stdout = "";
774
+ let stderr = "";
775
+ try {
776
+ note(t("instance.waiting"));
777
+ await waitForRunning(client, uuid, {
778
+ ...options.signal ? { signal: options.signal } : {},
779
+ onPoll: (status) => debug(`\u72B6\u6001\uFF1A${status}`)
780
+ });
781
+ success(t("instance.ready"));
782
+ if (options.sync) {
783
+ note(t("run.syncing"));
784
+ uploaded = await push(client, uuid, options.sync, workdir, {
785
+ ...options.signal ? { signal: options.signal } : {},
786
+ onProgress: ({ file, index, total }) => debug(`\u2191 [${index}/${total}] ${file}`)
787
+ });
788
+ success(`\u5DF2\u4E0A\u4F20 ${uploaded.files} \u4E2A\u6587\u4EF6\u5230 ${workdir}`);
789
+ }
790
+ note(t("run.executing"));
791
+ const result = await execCommand(client, uuid, options.command, {
792
+ capture: true,
793
+ // Same rule as `autodl exec`: stdout only when it isn't carrying the payload.
794
+ stdout: isJson() ? process.stderr : process.stdout,
795
+ stderr: process.stderr,
796
+ cwd: options.sync ? workdir : void 0,
797
+ ...options.env ? { env: options.env } : {},
798
+ ...options.commandTimeoutMs !== void 0 ? { timeoutMs: options.commandTimeoutMs } : {},
799
+ ...options.signal ? { signal: options.signal } : {}
800
+ });
801
+ exitCode = result.exitCode;
802
+ stdout = result.stdout;
803
+ stderr = result.stderr;
804
+ if (options.pullFrom) {
805
+ const target = options.pullTo ?? "./autodl-output";
806
+ note(t("run.pulling"));
807
+ await mkdir2(target, { recursive: true });
808
+ downloaded = await pull(client, uuid, options.pullFrom, target, {
809
+ ...options.signal ? { signal: options.signal } : {},
810
+ onProgress: ({ file, index, total }) => debug(`\u2193 [${index}/${total}] ${file}`)
811
+ });
812
+ success(`\u5DF2\u56DE\u4F20 ${downloaded.files} \u4E2A\u6587\u4EF6\u5230 ${target}`);
813
+ }
814
+ } finally {
815
+ await finish2(client, uuid, onFinish);
816
+ }
817
+ return {
818
+ instanceUuid: uuid,
819
+ exitCode,
820
+ stdout,
821
+ stderr,
822
+ uploaded,
823
+ downloaded,
824
+ finalAction: onFinish,
825
+ durationMs: Date.now() - startedAt
826
+ };
827
+ }
828
+ async function finish2(client, uuid, action) {
829
+ if (action === "keep") {
830
+ warn(`\u5B9E\u4F8B ${uuid} \u4ECD\u5728\u8FD0\u884C\uFF08--on-finish keep\uFF09\uFF0C\u8BB0\u5F97\u624B\u52A8\u5173\u673A\uFF1Aautodl stop ${uuid}`);
831
+ return;
832
+ }
833
+ note(t("run.cleanup"));
834
+ try {
835
+ const status = await getInstanceStatus(client, uuid).catch(() => "unknown");
836
+ if (status !== "shutdown" && status !== "shutting_down") {
837
+ await powerOffInstance(client, uuid);
838
+ }
839
+ success(`\u5B9E\u4F8B ${uuid} \u5DF2\u5173\u673A\uFF0C\u8BA1\u8D39\u5DF2\u505C\u6B62`);
840
+ } catch (err) {
841
+ warn(`\u81EA\u52A8\u5173\u673A\u5931\u8D25\uFF1A${err.message}`);
842
+ warn(`\u8BF7\u7ACB\u5373\u624B\u52A8\u5904\u7406\uFF1Aautodl stop ${uuid}`);
843
+ return;
844
+ }
845
+ if (action === "release") {
846
+ try {
847
+ await waitForShutdown(client, uuid, { timeoutMs: 10 * 6e4 });
848
+ await releaseInstance(client, uuid);
849
+ untrackInstance(uuid);
850
+ success(`\u5B9E\u4F8B ${uuid} \u5DF2\u91CA\u653E`);
851
+ } catch (err) {
852
+ warn(`\u91CA\u653E\u5931\u8D25\uFF08\u5B9E\u4F8B\u5DF2\u5173\u673A\uFF0C\u4E0D\u518D\u8BA1\u8D39\uFF09\uFF1A${err.message}`);
853
+ warn(`\u7A0D\u540E\u53EF\u91CD\u8BD5\uFF1Aautodl rm ${uuid} --yes`);
854
+ }
855
+ } else {
856
+ untrackInstance(uuid);
857
+ }
858
+ }
859
+
860
+ // src/mcp/server.ts
861
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
862
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
863
+ import { z } from "zod";
864
+ var DEFAULT_MCP_TTL = "2h";
865
+ function ok(data) {
866
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
867
+ }
868
+ function fail(err) {
869
+ const error = toAutoDLError(err);
870
+ return {
871
+ content: [
872
+ {
873
+ type: "text",
874
+ text: JSON.stringify({ ok: false, error: error.toJSON() }, null, 2)
875
+ }
876
+ ],
877
+ isError: true
878
+ };
879
+ }
880
+ function tool(handler) {
881
+ return async (input) => {
882
+ try {
883
+ return ok({ ok: true, data: await handler(input) });
884
+ } catch (err) {
885
+ return fail(err);
886
+ }
887
+ };
888
+ }
889
+ function buildServer(context) {
890
+ const server = new McpServer(
891
+ { name: "autodl-cli", version: VERSION },
892
+ {
893
+ instructions: [
894
+ "\u901A\u8FC7 AutoDL \u5B98\u65B9\u5F00\u653E API \u7BA1\u7406 GPU \u5B9E\u4F8B\uFF1A\u521B\u5EFA\u3001\u5F00\u5173\u673A\u3001SSH \u6267\u884C\u547D\u4EE4\u3001\u4F20\u6587\u4EF6\u3002",
895
+ "\u91CD\u8981\uFF1AAutoDL \u6309\u5B9E\u4F8B\u5F00\u673A\u65F6\u957F\u8BA1\u8D39\uFF0C\u4E0E\u662F\u5426\u4F7F\u7528 GPU \u65E0\u5173\u3002\u7528\u5B8C\u8BF7\u7ACB\u523B\u8C03\u7528 autodl_power_off\u3002",
896
+ "\u521B\u5EFA\u5B9E\u4F8B\u9ED8\u8BA4\u5E26 2 \u5C0F\u65F6 TTL \u515C\u5E95\u81EA\u52A8\u5173\u673A\uFF1B\u5982\u679C\u4EFB\u52A1\u66F4\u957F\u8BF7\u663E\u5F0F\u4F20 ttl\u3002",
897
+ "\u5F00\u5173\u673A\u540E SSH \u7AEF\u53E3\u548C\u5BC6\u7801\u4F1A\u53D8\u5316\uFF0C\u4E0D\u8981\u7F13\u5B58 autodl_get_instance \u7684\u7ED3\u679C\u3002"
898
+ ].join("\n")
899
+ }
900
+ );
901
+ const { client } = context;
902
+ server.registerTool(
903
+ "autodl_account_info",
904
+ {
905
+ title: "\u67E5\u8BE2\u8D26\u53F7\u4F59\u989D",
906
+ description: "\u8FD4\u56DE AutoDL \u8D26\u53F7\u7684\u53EF\u7528\u4F59\u989D\u3001\u4EE3\u91D1\u5238\u4F59\u989D\u548C\u7D2F\u8BA1\u6D88\u8D39\uFF08\u5355\u4F4D\uFF1A\u5143\uFF09\u3002",
907
+ inputSchema: {},
908
+ annotations: { readOnlyHint: true }
909
+ },
910
+ tool(async () => {
911
+ const balance = await getBalance(client);
912
+ return { ...balance, spendableYuan: balance.balanceYuan + balance.voucherYuan };
913
+ })
914
+ );
915
+ server.registerTool(
916
+ "autodl_list_instances",
917
+ {
918
+ title: "\u5217\u51FA\u5B9E\u4F8B",
919
+ description: "\u5217\u51FA\u8D26\u53F7\u4E0B\u6240\u6709\u5B9E\u4F8B\u53CA\u5176\u72B6\u6001\u3002\u53EF\u7528 status \u8FC7\u6EE4\uFF0C\u5982 running / shutdown\u3002",
920
+ inputSchema: { status: z.string().optional().describe("\u6309\u72B6\u6001\u8FC7\u6EE4\uFF0C\u4F8B\u5982 running") },
921
+ annotations: { readOnlyHint: true }
922
+ },
923
+ tool(async ({ status }) => {
924
+ const instances = await listAllInstances(client);
925
+ return status ? instances.filter((i) => i.status === status) : instances;
926
+ })
927
+ );
928
+ server.registerTool(
929
+ "autodl_get_instance",
930
+ {
931
+ title: "\u67E5\u8BE2\u5B9E\u4F8B\u8BE6\u60C5",
932
+ description: "\u8FD4\u56DE\u5B9E\u4F8B\u8BE6\u60C5\u4E0E\u5B9E\u65F6 SSH \u8FDE\u63A5\u4FE1\u606F\u3002\u6CE8\u610F\uFF1A\u5B9E\u4F8B\u6BCF\u6B21\u5F00\u5173\u673A\u540E SSH \u7AEF\u53E3\u548C root \u5BC6\u7801\u90FD\u4F1A\u53D8\u5316\uFF0C\u52A1\u5FC5\u6BCF\u6B21\u91CD\u65B0\u8C03\u7528\u800C\u4E0D\u8981\u7F13\u5B58\u3002",
933
+ inputSchema: {
934
+ instance_uuid: z.string().describe("\u5B9E\u4F8B ID\uFF0C\u4F8B\u5982 pro-76419909953e"),
935
+ reveal_password: z.boolean().optional().describe("\u662F\u5426\u8FD4\u56DE root \u5BC6\u7801\u660E\u6587\uFF0C\u9ED8\u8BA4\u8131\u654F\u4E3A ***")
936
+ },
937
+ annotations: { readOnlyHint: true }
938
+ },
939
+ tool(
940
+ async ({
941
+ instance_uuid,
942
+ reveal_password
943
+ }) => {
944
+ const instance = await findInstance(client, instance_uuid);
945
+ const snapshot = await getInstanceSnapshot(client, instance_uuid).catch(() => null);
946
+ return {
947
+ instance,
948
+ snapshot: snapshot ? reveal_password ? snapshot : redactSnapshot(snapshot) : null
949
+ };
950
+ }
951
+ )
952
+ );
953
+ server.registerTool(
954
+ "autodl_create_instance",
955
+ {
956
+ title: "\u521B\u5EFA GPU \u5B9E\u4F8B",
957
+ description: [
958
+ "\u521B\u5EFA\u4E00\u53F0\u6309\u91CF\u8BA1\u8D39\u7684 AutoDL Pro \u5B9E\u4F8B\u5E76\u53EF\u9009\u7B49\u5F85\u5176\u5C31\u7EEA\u3002",
959
+ "\u4F1A\u5148\u68C0\u67E5\u4F59\u989D\uFF1B\u4F59\u989D\u4E0D\u8DB3\u4F1A\u76F4\u63A5\u62D2\u7EDD\u3002",
960
+ "\u9ED8\u8BA4\u5E26 2 \u5C0F\u65F6 TTL \u515C\u5E95\u81EA\u52A8\u5173\u673A\u2014\u2014\u957F\u4EFB\u52A1\u8BF7\u663E\u5F0F\u52A0\u5927 ttl\uFF0C\u4E0D\u8981\u5173\u95ED\u5B83\u3002",
961
+ "\u5B98\u65B9 API \u6CA1\u6709\u5E93\u5B58\u67E5\u8BE2\u63A5\u53E3\uFF0C\u65E0\u8D27\u65F6\u4F1A\u8FD4\u56DE NO_STOCK\uFF0C\u6362 GPU \u89C4\u683C\u6216\u5730\u533A\u91CD\u8BD5\u5373\u53EF\u3002"
962
+ ].join("\n"),
963
+ inputSchema: {
964
+ gpu: z.string().describe("GPU \u89C4\u683C\uFF0C\u5982 4090 / pro6000-p\uFF0C\u7528 autodl_list_gpu_specs \u67E5\u770B"),
965
+ gpu_num: z.number().int().min(1).max(4).optional().describe("GPU \u6570\u91CF 1-4\uFF0C\u9ED8\u8BA4 1"),
966
+ image: z.string().optional().describe("\u955C\u50CF UUID\uFF0C\u9ED8\u8BA4 PyTorch 2.0 / CUDA 11.8"),
967
+ regions: z.array(z.string()).optional().describe("\u4F18\u5148\u5730\u533A\u4EE3\u7801\u5217\u8868"),
968
+ disk_gb: z.number().int().min(0).max(500).optional().describe("\u7CFB\u7EDF\u76D8\u6269\u5BB9 GB\uFF0C\u9ED8\u8BA4 0"),
969
+ name: z.string().optional().describe("\u5B9E\u4F8B\u540D\u79F0"),
970
+ ttl: z.string().optional().describe("\u5230\u671F\u81EA\u52A8\u5173\u673A\u65F6\u957F\uFF0C\u5982 2h / 90m\uFF0C\u9ED8\u8BA4 2h"),
971
+ wait: z.boolean().optional().describe("\u662F\u5426\u7B49\u5F85\u5B9E\u4F8B\u8FDB\u5165 running\uFF0C\u9ED8\u8BA4 true")
972
+ },
973
+ annotations: { destructiveHint: false, idempotentHint: false }
974
+ },
975
+ tool(
976
+ async (input) => {
977
+ const spec = resolveGpuSpec(input.gpu);
978
+ if (!spec) {
979
+ throw new UsageError(`\u672A\u77E5\u7684 GPU \u89C4\u683C "${input.gpu}"`, {
980
+ hint: "\u8C03\u7528 autodl_list_gpu_specs \u67E5\u770B\u5168\u90E8\u53EF\u7528\u89C4\u683C\u3002"
981
+ });
982
+ }
983
+ const image = findBaseImage(input.image ?? DEFAULT_BASE_IMAGE);
984
+ const imageUuid = image?.uuid ?? input.image ?? DEFAULT_BASE_IMAGE;
985
+ const regions = (input.regions ?? []).map((value) => {
986
+ const region = resolveRegion(value);
987
+ if (!region) throw new UsageError(`\u672A\u77E5\u7684\u5730\u533A "${value}"`);
988
+ return region.id;
989
+ });
990
+ const ttlSeconds = parseDuration(input.ttl ?? DEFAULT_MCP_TTL);
991
+ await assertBudget(client);
992
+ const startCommand = composeStartCommand(ttlSeconds, void 0);
993
+ const uuid = await createInstance(client, {
994
+ gpuSpec: spec.id,
995
+ gpuNum: input.gpu_num ?? 1,
996
+ imageUuid,
997
+ cudaFrom: parseCudaVersion(image?.cuda ?? "11.8"),
998
+ expandSystemDiskGb: input.disk_gb ?? 0,
999
+ ...regions.length ? { regions } : {},
1000
+ ...input.name ? { name: input.name } : {},
1001
+ ...startCommand ? { startCommand } : {}
1002
+ });
1003
+ recordTTL({
1004
+ uuid,
1005
+ ...input.name ? { name: input.name } : {},
1006
+ ttlSeconds,
1007
+ inInstanceTimer: true
1008
+ });
1009
+ let status = "creating";
1010
+ if (input.wait !== false) status = await waitForRunning(client, uuid);
1011
+ return {
1012
+ instance_uuid: uuid,
1013
+ status,
1014
+ gpu_spec: spec.id,
1015
+ gpu_num: input.gpu_num ?? 1,
1016
+ image_uuid: imageUuid,
1017
+ ttl_seconds: ttlSeconds,
1018
+ note: `\u5B9E\u4F8B\u5C06\u5728 ${input.ttl ?? DEFAULT_MCP_TTL} \u540E\u81EA\u52A8\u5173\u673A\u3002\u7528\u5B8C\u8BF7\u5C3D\u5FEB\u8C03\u7528 autodl_power_off \u505C\u6B62\u8BA1\u8D39\u3002`
1019
+ };
1020
+ }
1021
+ )
1022
+ );
1023
+ server.registerTool(
1024
+ "autodl_power_on",
1025
+ {
1026
+ title: "\u5F00\u673A",
1027
+ description: "\u542F\u52A8\u4E00\u53F0\u5DF2\u5173\u673A\u7684\u5B9E\u4F8B\uFF0C\u5E76\u53EF\u9009\u7B49\u5F85\u5176\u5C31\u7EEA\u3002\u5F00\u673A\u540E SSH \u7AEF\u53E3\u548C\u5BC6\u7801\u4F1A\u53D8\u5316\u3002",
1028
+ inputSchema: {
1029
+ instance_uuid: z.string(),
1030
+ ttl: z.string().optional().describe("\u5F00\u673A\u540E\u8BBE\u7F6E\u7684\u81EA\u52A8\u5173\u673A\u65F6\u957F\uFF0C\u9ED8\u8BA4 2h"),
1031
+ wait: z.boolean().optional().describe("\u662F\u5426\u7B49\u5F85 running\uFF0C\u9ED8\u8BA4 true")
1032
+ }
1033
+ },
1034
+ tool(async (input) => {
1035
+ const ttlSeconds = parseDuration(input.ttl ?? DEFAULT_MCP_TTL);
1036
+ await powerOnInstance(client, input.instance_uuid, {
1037
+ startCommand: composeStartCommand(ttlSeconds, void 0)
1038
+ });
1039
+ let status = "starting";
1040
+ let armed = false;
1041
+ if (input.wait !== false) {
1042
+ status = await waitForRunning(client, input.instance_uuid);
1043
+ armed = await armTTLOverSSH(client, input.instance_uuid, ttlSeconds);
1044
+ recordTTL({ uuid: input.instance_uuid, ttlSeconds, inInstanceTimer: armed });
1045
+ }
1046
+ return {
1047
+ instance_uuid: input.instance_uuid,
1048
+ status,
1049
+ ttl_seconds: ttlSeconds,
1050
+ ttl_armed: armed
1051
+ };
1052
+ })
1053
+ );
1054
+ server.registerTool(
1055
+ "autodl_power_off",
1056
+ {
1057
+ title: "\u5173\u673A",
1058
+ description: "\u5173\u95ED\u5B9E\u4F8B\uFF0C\u7ACB\u5373\u505C\u6B62\u8BA1\u8D39\u3002\u6570\u636E\u5168\u90E8\u4FDD\u7559\uFF0C\u968F\u65F6\u53EF\u4EE5\u518D\u6B21\u5F00\u673A\u3002",
1059
+ inputSchema: { instance_uuid: z.string() }
1060
+ },
1061
+ tool(async ({ instance_uuid }) => {
1062
+ await powerOffInstance(client, instance_uuid);
1063
+ untrackInstance(instance_uuid);
1064
+ return { instance_uuid, stopped: true };
1065
+ })
1066
+ );
1067
+ server.registerTool(
1068
+ "autodl_release_instance",
1069
+ {
1070
+ title: "\u91CA\u653E\u5B9E\u4F8B\uFF08\u4E0D\u53EF\u9006\uFF09",
1071
+ description: "\u6C38\u4E45\u91CA\u653E\u5B9E\u4F8B\uFF0C\u6240\u6709\u6570\u636E\u4F1A\u88AB\u6E05\u7A7A\u4E14\u65E0\u6CD5\u6062\u590D\u3002\u5FC5\u987B\u5148\u5173\u673A\u3002\u9700\u8981\u663E\u5F0F\u4F20 confirm=true \u624D\u4F1A\u6267\u884C\u3002",
1072
+ inputSchema: {
1073
+ instance_uuid: z.string(),
1074
+ confirm: z.boolean().describe("\u5FC5\u987B\u663E\u5F0F\u4F20 true\uFF0C\u786E\u8BA4\u7406\u89E3\u6570\u636E\u5C06\u88AB\u6C38\u4E45\u6E05\u7A7A")
1075
+ },
1076
+ annotations: { destructiveHint: true, idempotentHint: false }
1077
+ },
1078
+ tool(async ({ instance_uuid, confirm }) => {
1079
+ if (confirm !== true) {
1080
+ throw new UsageError("\u91CA\u653E\u5B9E\u4F8B\u662F\u4E0D\u53EF\u9006\u64CD\u4F5C\uFF0C\u5FC5\u987B\u663E\u5F0F\u4F20 confirm=true", {
1081
+ hint: "\u786E\u8BA4\u8981\u6C38\u4E45\u5220\u9664\u8BE5\u5B9E\u4F8B\u7684\u5168\u90E8\u6570\u636E\u540E\uFF0C\u518D\u5E26 confirm: true \u91CD\u65B0\u8C03\u7528\u3002"
1082
+ });
1083
+ }
1084
+ const status = await getInstanceStatus(client, instance_uuid);
1085
+ if (status !== "shutdown") {
1086
+ await powerOffInstance(client, instance_uuid);
1087
+ }
1088
+ await releaseInstance(client, instance_uuid);
1089
+ untrackInstance(instance_uuid);
1090
+ return { instance_uuid, released: true };
1091
+ })
1092
+ );
1093
+ server.registerTool(
1094
+ "autodl_exec",
1095
+ {
1096
+ title: "\u5728\u5B9E\u4F8B\u4E0A\u6267\u884C\u547D\u4EE4",
1097
+ description: "\u901A\u8FC7 SSH \u5728\u5B9E\u4F8B\u4E0A\u6267\u884C shell \u547D\u4EE4\uFF0C\u8FD4\u56DE stdout / stderr / \u9000\u51FA\u7801\u3002\u6BCF\u6B21\u90FD\u4F1A\u91CD\u65B0\u83B7\u53D6 SSH \u51ED\u8BC1\uFF0C\u56E0\u6B64\u5B9E\u4F8B\u91CD\u542F\u540E\u4F9D\u7136\u53EF\u7528\u3002",
1098
+ inputSchema: {
1099
+ instance_uuid: z.string(),
1100
+ command: z.string().describe("\u8981\u6267\u884C\u7684 shell \u547D\u4EE4"),
1101
+ cwd: z.string().optional().describe("\u8FDC\u7A0B\u5DE5\u4F5C\u76EE\u5F55"),
1102
+ timeout: z.string().optional().describe("\u8D85\u65F6\u65F6\u95F4\uFF0C\u5982 30m\uFF0C\u9ED8\u8BA4 30m"),
1103
+ start_if_stopped: z.boolean().optional().describe("\u5B9E\u4F8B\u672A\u8FD0\u884C\u65F6\u662F\u5426\u81EA\u52A8\u5F00\u673A\uFF0C\u9ED8\u8BA4 false")
1104
+ }
1105
+ },
1106
+ tool(
1107
+ async (input) => {
1108
+ const result = await execCommand(client, input.instance_uuid, input.command, {
1109
+ capture: true,
1110
+ autoStart: input.start_if_stopped ?? false,
1111
+ timeoutMs: parseDuration(input.timeout ?? "30m") * 1e3,
1112
+ ...input.cwd ? { cwd: input.cwd } : {}
1113
+ });
1114
+ return {
1115
+ instance_uuid: input.instance_uuid,
1116
+ exit_code: result.exitCode,
1117
+ signal: result.signal,
1118
+ stdout: result.stdout,
1119
+ stderr: result.stderr
1120
+ };
1121
+ }
1122
+ )
1123
+ );
1124
+ server.registerTool(
1125
+ "autodl_upload",
1126
+ {
1127
+ title: "\u4E0A\u4F20\u6587\u4EF6\u5230\u5B9E\u4F8B",
1128
+ description: "\u901A\u8FC7 SFTP \u4E0A\u4F20\u672C\u5730\u6587\u4EF6\u6216\u76EE\u5F55\u5230\u5B9E\u4F8B\u3002\u76EE\u5F55\u4F1A\u81EA\u52A8\u8DF3\u8FC7 .git / node_modules \u7B49\u3002",
1129
+ inputSchema: {
1130
+ instance_uuid: z.string(),
1131
+ local_path: z.string().describe("\u672C\u5730\u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84"),
1132
+ remote_path: z.string().optional().describe("\u8FDC\u7A0B\u76EE\u6807\u8DEF\u5F84\uFF0C\u9ED8\u8BA4 /root/autodl-tmp/autodl-cli\uFF08\u6570\u636E\u76D8\uFF09")
1133
+ }
1134
+ },
1135
+ tool(async (input) => {
1136
+ const remote = input.remote_path ?? "/root/autodl-tmp/autodl-cli";
1137
+ const summary = await push(client, input.instance_uuid, input.local_path, remote);
1138
+ return { instance_uuid: input.instance_uuid, remote_path: remote, ...summary };
1139
+ })
1140
+ );
1141
+ server.registerTool(
1142
+ "autodl_download",
1143
+ {
1144
+ title: "\u4ECE\u5B9E\u4F8B\u4E0B\u8F7D\u6587\u4EF6",
1145
+ description: "\u901A\u8FC7 SFTP \u628A\u5B9E\u4F8B\u4E0A\u7684\u6587\u4EF6\u6216\u76EE\u5F55\u4E0B\u8F7D\u5230\u672C\u5730\u3002",
1146
+ inputSchema: {
1147
+ instance_uuid: z.string(),
1148
+ remote_path: z.string(),
1149
+ local_path: z.string().optional().describe("\u672C\u5730\u76EE\u6807\u76EE\u5F55\uFF0C\u9ED8\u8BA4 ./autodl-output")
1150
+ }
1151
+ },
1152
+ tool(async (input) => {
1153
+ const local = input.local_path ?? "./autodl-output";
1154
+ const summary = await pull(client, input.instance_uuid, input.remote_path, local);
1155
+ return { instance_uuid: input.instance_uuid, local_path: local, ...summary };
1156
+ })
1157
+ );
1158
+ server.registerTool(
1159
+ "autodl_run",
1160
+ {
1161
+ title: "\u4E00\u952E\u8DD1\u4EFB\u52A1",
1162
+ description: [
1163
+ "\u7AEF\u5230\u7AEF\u5DE5\u4F5C\u6D41\uFF1A\u521B\u5EFA\u5B9E\u4F8B \u2192 \u7B49\u5F85\u5C31\u7EEA \u2192 \u4E0A\u4F20\u4EE3\u7801 \u2192 \u8FDC\u7A0B\u6267\u884C \u2192 \u56DE\u4F20\u4EA7\u7269 \u2192 \u81EA\u52A8\u5173\u673A\u3002",
1164
+ "\u9002\u5408\u201C\u8DD1\u4E00\u6B21\u5C31\u8D70\u201D\u7684\u4EFB\u52A1\uFF0C\u65E0\u9700\u81EA\u5DF1\u7F16\u6392\u751F\u547D\u5468\u671F\uFF0C\u4E5F\u4E0D\u4F1A\u5FD8\u8BB0\u5173\u673A\u3002",
1165
+ "\u6CE8\u610F\uFF1A\u8FD9\u4E2A\u8C03\u7528\u4F1A\u963B\u585E\u5230\u4EFB\u52A1\u7ED3\u675F\uFF0C\u8BF7\u4E3A ttl \u548C timeout \u7559\u8DB3\u4F59\u91CF\u3002"
1166
+ ].join("\n"),
1167
+ inputSchema: {
1168
+ command: z.string().describe("\u8981\u5728\u5B9E\u4F8B\u4E0A\u6267\u884C\u7684\u547D\u4EE4"),
1169
+ gpu: z.string().describe("GPU \u89C4\u683C"),
1170
+ gpu_num: z.number().int().min(1).max(4).optional(),
1171
+ image: z.string().optional(),
1172
+ regions: z.array(z.string()).optional(),
1173
+ sync: z.string().optional().describe("\u6267\u884C\u524D\u4E0A\u4F20\u7684\u672C\u5730\u76EE\u5F55"),
1174
+ workdir: z.string().optional().describe("\u8FDC\u7A0B\u5DE5\u4F5C\u76EE\u5F55\uFF0C\u9ED8\u8BA4 /root/autodl-tmp/autodl-cli\uFF08\u6570\u636E\u76D8\uFF09"),
1175
+ pull_from: z.string().optional().describe("\u6267\u884C\u540E\u56DE\u4F20\u7684\u8FDC\u7A0B\u8DEF\u5F84"),
1176
+ pull_to: z.string().optional().describe("\u56DE\u4F20\u4EA7\u7269\u7684\u672C\u5730\u76EE\u5F55"),
1177
+ ttl: z.string().optional().describe("\u515C\u5E95\u81EA\u52A8\u5173\u673A\u65F6\u957F\uFF0C\u9ED8\u8BA4 4h"),
1178
+ timeout: z.string().optional().describe("\u8FDC\u7A0B\u547D\u4EE4\u8D85\u65F6\u65F6\u95F4"),
1179
+ on_finish: z.enum(["poweroff", "release", "keep"]).optional().describe("\u7ED3\u675F\u540E\u52A8\u4F5C\uFF0C\u9ED8\u8BA4 poweroff")
1180
+ },
1181
+ annotations: { destructiveHint: false, idempotentHint: false }
1182
+ },
1183
+ tool(
1184
+ async (input) => {
1185
+ const result = await runWorkflow(client, {
1186
+ command: input.command,
1187
+ gpu: input.gpu,
1188
+ ...input.gpu_num !== void 0 ? { gpuNum: input.gpu_num } : {},
1189
+ ...input.image ? { image: input.image } : {},
1190
+ ...input.regions ? { regions: input.regions } : {},
1191
+ ...input.sync ? { sync: input.sync } : {},
1192
+ ...input.workdir ? { workdir: input.workdir } : {},
1193
+ ...input.pull_from ? { pullFrom: input.pull_from } : {},
1194
+ ...input.pull_to ? { pullTo: input.pull_to } : {},
1195
+ ttlSeconds: parseDuration(input.ttl ?? "4h"),
1196
+ ...input.timeout ? { commandTimeoutMs: parseDuration(input.timeout) * 1e3 } : {},
1197
+ onFinish: input.on_finish ?? "poweroff"
1198
+ });
1199
+ return {
1200
+ instance_uuid: result.instanceUuid,
1201
+ exit_code: result.exitCode,
1202
+ stdout: result.stdout,
1203
+ stderr: result.stderr,
1204
+ uploaded: result.uploaded,
1205
+ downloaded: result.downloaded,
1206
+ final_action: result.finalAction,
1207
+ duration_ms: result.durationMs
1208
+ };
1209
+ }
1210
+ )
1211
+ );
1212
+ server.registerTool(
1213
+ "autodl_gpu_stock",
1214
+ {
1215
+ title: "\u67E5\u8BE2 GPU \u5B9E\u65F6\u5E93\u5B58",
1216
+ description: [
1217
+ "\u6309\u5730\u533A\u67E5\u8BE2 GPU \u7A7A\u95F2\u6570\u91CF\uFF0C\u7528\u4E8E\u5728\u521B\u5EFA\u5B9E\u4F8B\u524D\u6311\u4E00\u4E2A\u6709\u8D27\u7684\u5730\u533A\u3002",
1218
+ "\u6CE8\u610F\uFF1A\u6570\u636E\u6765\u81EA AutoDL \u7684\u300C\u5F39\u6027\u90E8\u7F72 GPU \u5E93\u5B58\u300D\u63A5\u53E3\uFF0C\u4E0E Pro \u5B9E\u4F8B\u80FD\u5426\u521B\u5EFA\u6210\u529F\u6CA1\u6709\u5B98\u65B9\u4FDD\u8BC1\uFF0C\u4EC5\u4F9B\u62E9\u4F18\u53C2\u8003\u2014\u2014\u4E0D\u8981\u636E\u6B64\u5224\u5B9A\u300C\u4E00\u5B9A\u521B\u5EFA\u4E0D\u4E86\u300D\u3002",
1219
+ "autodl_create_instance \u9ED8\u8BA4\u5DF2\u7ECF\u81EA\u52A8\u505A\u4E86\u8FD9\u4EF6\u4E8B\uFF0C\u901A\u5E38\u4E0D\u9700\u8981\u5355\u72EC\u8C03\u7528\u3002"
1220
+ ].join("\n"),
1221
+ inputSchema: {
1222
+ gpu: z.string().optional().describe("\u53EA\u770B\u67D0\u4E2A GPU \u89C4\u683C\uFF0C\u5982 4090 / pro6000-p"),
1223
+ regions: z.array(z.string()).optional().describe("\u5730\u533A\u4EE3\u7801\u5217\u8868\uFF0C\u9ED8\u8BA4\u67E5\u5168\u90E8")
1224
+ },
1225
+ annotations: { readOnlyHint: true }
1226
+ },
1227
+ tool(async (input) => {
1228
+ const spec = input.gpu ? resolveGpuSpec(input.gpu) : void 0;
1229
+ if (input.gpu && !spec) {
1230
+ throw new UsageError(`\u672A\u77E5\u7684 GPU \u89C4\u683C "${input.gpu}"`, {
1231
+ hint: "\u8C03\u7528 autodl_list_gpu_specs \u67E5\u770B\u5168\u90E8\u53EF\u7528\u89C4\u683C\u3002"
1232
+ });
1233
+ }
1234
+ const { snapshots, failures } = await getStockByRegion(client, {
1235
+ ...input.regions?.length ? { regions: input.regions } : {},
1236
+ ...spec ? { gpuNames: [spec.stockName] } : {}
1237
+ });
1238
+ const rentable = new Map(GPU_SPECS.map((s) => [s.stockName, s.id]));
1239
+ const rows = snapshots.flatMap(
1240
+ (snapshot) => snapshot.entries.map((entry) => ({
1241
+ region: snapshot.regionId,
1242
+ region_name: snapshot.regionName,
1243
+ gpu_name: entry.gpuName,
1244
+ // Null means this card cannot be rented through the open API at all.
1245
+ gpu_spec: rentable.get(entry.gpuName) ?? null,
1246
+ idle: entry.idle,
1247
+ total: entry.total
1248
+ }))
1249
+ ).sort((a, b) => b.idle - a.idle);
1250
+ return { rows, failures };
1251
+ })
1252
+ );
1253
+ server.registerTool(
1254
+ "autodl_deploy",
1255
+ {
1256
+ title: "\u90E8\u7F72\u4EE3\u7801\u6258\u7BA1\u5E73\u53F0\u7684\u9879\u76EE",
1257
+ description: [
1258
+ "\u628A\u4E00\u4E2A GitHub / Gitee / GitLab \u4ED3\u5E93\u90E8\u7F72\u5230 AutoDL \u5B9E\u4F8B\u4E0A\uFF1A\u521B\u5EFA\u5B9E\u4F8B \u2192 \u62C9\u4EE3\u7801 \u2192 \u81EA\u52A8\u63A2\u6D4B\u5E76\u5B89\u88C5\u4F9D\u8D56 \u2192 \u53EF\u9009\u542F\u52A8\u3002",
1259
+ "\u4E0E autodl_run \u7684\u5173\u952E\u533A\u522B\uFF1A\u7ED3\u675F\u65F6\u9ED8\u8BA4**\u5173\u673A\u800C\u4E0D\u91CA\u653E**\uFF0C\u5B9E\u4F8B\u6570\u636E\u5B8C\u6574\u4FDD\u7559\u3002",
1260
+ "\u4E0B\u6B21\u5E26 instance_uuid \u518D\u8C03\u4E00\u6B21\u5373\u53EF\u590D\u7528\u540C\u4E00\u53F0\u673A\u5668\uFF08\u5F00\u673A \u2192 git pull \u2192 \u91CD\u65B0\u90E8\u7F72\uFF09\uFF0C\u7701\u6389\u91CD\u5EFA\u73AF\u5883\u7684\u65F6\u95F4\u3002",
1261
+ "\u4EE3\u7801\u653E\u5728\u6570\u636E\u76D8 /root/autodl-tmp/<\u4ED3\u5E93\u540D>\uFF08\u7CFB\u7EDF\u76D8\u53EA\u6709 30G\uFF09\u3002GitHub \u4F1A\u81EA\u52A8\u8D70\u5B66\u672F\u52A0\u901F\u3002",
1262
+ "detach=true \u65F6\u4F1A\u540E\u53F0\u542F\u52A8\u5E76\u8BA9\u5B9E\u4F8B\u4FDD\u6301\u8FD0\u884C\u2014\u2014\u8FD9\u79CD\u60C5\u51B5\u4E0B\u7528\u5B8C\u5FC5\u987B\u81EA\u5DF1\u8C03 autodl_power_off\uFF0C\u5426\u5219\u4F1A\u4E00\u76F4\u8BA1\u8D39\u3002"
1263
+ ].join("\n"),
1264
+ inputSchema: {
1265
+ repo: z.string().describe("\u4ED3\u5E93\u5730\u5740\uFF1Ahttps://github.com/owner/repo\u3001git@... \u6216 owner/repo"),
1266
+ gpu: z.string().optional().describe("GPU \u89C4\u683C\uFF1B\u4E0D\u4F20 instance_uuid \u65F6\u5FC5\u586B"),
1267
+ instance_uuid: z.string().optional().describe("\u590D\u7528\u5DF2\u6709\u5B9E\u4F8B\uFF08\u4E4B\u524D\u90E8\u7F72\u8FC7\u5E76\u5173\u673A\u7684\u90A3\u53F0\uFF09"),
1268
+ branch: z.string().optional(),
1269
+ dir: z.string().optional().describe("\u8FDC\u7A0B\u4EE3\u7801\u76EE\u5F55\uFF0C\u9ED8\u8BA4 /root/autodl-tmp/<\u4ED3\u5E93\u540D>"),
1270
+ setup: z.string().optional().describe("\u81EA\u5B9A\u4E49\u4F9D\u8D56\u5B89\u88C5\u547D\u4EE4\uFF0C\u8986\u76D6\u81EA\u52A8\u63A2\u6D4B"),
1271
+ no_setup: z.boolean().optional().describe("\u8DF3\u8FC7\u4F9D\u8D56\u5B89\u88C5"),
1272
+ start: z.string().optional().describe("\u4F9D\u8D56\u88C5\u597D\u540E\u6267\u884C\u7684\u542F\u52A8\u547D\u4EE4"),
1273
+ detach: z.boolean().optional().describe("\u540E\u53F0\u542F\u52A8\u5E76\u4FDD\u6301\u5B9E\u4F8B\u8FD0\u884C\uFF0C\u9ED8\u8BA4 false"),
1274
+ git_token: z.string().optional().describe("\u79C1\u6709\u4ED3\u5E93\u51ED\u8BC1\uFF08\u654F\u611F\uFF0C\u4E0D\u4F1A\u88AB\u56DE\u663E\uFF09"),
1275
+ regions: z.array(z.string()).optional(),
1276
+ ttl: z.string().optional().describe("\u5230\u671F\u81EA\u52A8\u5173\u673A\u65F6\u957F\uFF0C\u9ED8\u8BA4 4h"),
1277
+ on_finish: z.enum(["poweroff", "release", "keep"]).optional().describe("\u7ED3\u675F\u52A8\u4F5C\uFF0C\u9ED8\u8BA4 poweroff\uFF08\u5173\u673A\u4FDD\u7559\u6570\u636E\uFF09"),
1278
+ timeout: z.string().optional().describe("\u5355\u6761\u8FDC\u7A0B\u547D\u4EE4\u8D85\u65F6\u65F6\u95F4")
1279
+ },
1280
+ annotations: { destructiveHint: false, idempotentHint: false }
1281
+ },
1282
+ tool(
1283
+ async (input) => {
1284
+ const token = resolveGitToken(input.git_token);
1285
+ const result = await deployWorkflow(client, {
1286
+ repo: input.repo,
1287
+ ...input.gpu ? { gpu: input.gpu } : {},
1288
+ ...input.instance_uuid ? { instanceUuid: input.instance_uuid } : {},
1289
+ ...input.branch ? { branch: input.branch } : {},
1290
+ ...input.dir ? { dir: input.dir } : {},
1291
+ ...input.setup ? { setup: input.setup } : {},
1292
+ noSetup: input.no_setup === true,
1293
+ ...input.start ? { start: input.start } : {},
1294
+ detach: input.detach === true,
1295
+ ...token ? { gitToken: token } : {},
1296
+ ...input.regions?.length ? { regions: input.regions } : {},
1297
+ ttlSeconds: parseDuration(input.ttl ?? "4h"),
1298
+ onFinish: input.on_finish ?? "poweroff",
1299
+ ...input.timeout ? { commandTimeoutMs: parseDuration(input.timeout) * 1e3 } : {}
1300
+ });
1301
+ return {
1302
+ instance_uuid: result.instanceUuid,
1303
+ created: result.created,
1304
+ repo: result.repo,
1305
+ dir: result.dir,
1306
+ setup_command: result.setupCommand,
1307
+ start_command: result.startCommand,
1308
+ detached: result.detached,
1309
+ exit_code: result.exitCode,
1310
+ stdout: result.stdout,
1311
+ stderr: result.stderr,
1312
+ access: result.access,
1313
+ final_action: result.finalAction,
1314
+ duration_ms: result.durationMs,
1315
+ note: result.detached ? `\u5B9E\u4F8B\u4FDD\u6301\u8FD0\u884C\u4E2D\uFF0C\u7528\u5B8C\u8BF7\u8C03\u7528 autodl_power_off("${result.instanceUuid}") \u505C\u6B62\u8BA1\u8D39\u3002` : `\u5B9E\u4F8B\u5DF2\u5173\u673A\u3001\u6570\u636E\u4FDD\u7559\u3002\u590D\u7528\uFF1A\u518D\u6B21\u8C03\u7528\u672C\u5DE5\u5177\u5E76\u4F20 instance_uuid="${result.instanceUuid}"\u3002`
1316
+ };
1317
+ }
1318
+ )
1319
+ );
1320
+ server.registerTool(
1321
+ "autodl_list_gpu_specs",
1322
+ {
1323
+ title: "\u5217\u51FA\u53EF\u7528 GPU \u89C4\u683C",
1324
+ description: "\u5217\u51FA\u5B98\u65B9\u5F00\u653E API \u652F\u6301\u7684\u5168\u90E8 GPU \u89C4\u683C\u3002\u6CE8\u610F\u5B98\u65B9\u6CA1\u6709\u5E93\u5B58\u63A5\u53E3\uFF0C\u6709\u8D27\u4E0E\u5426\u53EA\u80FD\u521B\u5EFA\u65F6\u624D\u77E5\u9053\u3002",
1325
+ inputSchema: {},
1326
+ annotations: { readOnlyHint: true }
1327
+ },
1328
+ tool(async () => ({ gpu_specs: GPU_SPECS, regions: REGIONS }))
1329
+ );
1330
+ server.registerTool(
1331
+ "autodl_list_images",
1332
+ {
1333
+ title: "\u5217\u51FA\u955C\u50CF",
1334
+ description: "\u5217\u51FA\u5B98\u65B9\u516C\u5171\u57FA\u7840\u955C\u50CF\uFF0C\u6216\u8D26\u53F7\u4E0B\u7684\u79C1\u6709\u955C\u50CF\u3002",
1335
+ inputSchema: {
1336
+ kind: z.enum(["base", "private"]).optional().describe("\u9ED8\u8BA4 base\uFF08\u516C\u5171\u57FA\u7840\u955C\u50CF\uFF09")
1337
+ },
1338
+ annotations: { readOnlyHint: true }
1339
+ },
1340
+ tool(async ({ kind }) => {
1341
+ if (kind === "private") {
1342
+ const { images } = await listPrivateImages(client, { pageSize: 100 });
1343
+ return { kind: "private", images };
1344
+ }
1345
+ return { kind: "base", images: BASE_IMAGES };
1346
+ })
1347
+ );
1348
+ server.registerTool(
1349
+ "autodl_save_image",
1350
+ {
1351
+ title: "\u4FDD\u5B58\u5B9E\u4F8B\u4E3A\u79C1\u6709\u955C\u50CF",
1352
+ description: "\u628A\u5B9E\u4F8B\u5F53\u524D\u72B6\u6001\u4FDD\u5B58\u6210\u53EF\u590D\u7528\u7684\u79C1\u6709\u955C\u50CF\u3002\u4FDD\u5B58\u8FC7\u7A0B\u9700\u8981\u4E00\u6BB5\u65F6\u95F4\uFF0C\u4E4B\u540E\u7528 autodl_list_images \u67E5\u72B6\u6001\u3002",
1353
+ inputSchema: { instance_uuid: z.string(), image_name: z.string() }
1354
+ },
1355
+ tool(async ({ instance_uuid, image_name }) => ({
1356
+ instance_uuid,
1357
+ image_uuid: await saveImage(client, instance_uuid, image_name)
1358
+ }))
1359
+ );
1360
+ server.registerTool(
1361
+ "autodl_sweep_expired",
1362
+ {
1363
+ title: "\u6E05\u7406\u8D85\u65F6\u5B9E\u4F8B",
1364
+ description: "\u5173\u95ED\u672C\u673A\u8BB0\u5F55\u4E2D\u6240\u6709\u5DF2\u8D85\u8FC7 TTL \u4F46\u4ECD\u5728\u8FD0\u884C\u7684\u5B9E\u4F8B\u3002\u7528\u4E8E\u6536\u5C3E\u515C\u5E95\u3002",
1365
+ inputSchema: {}
1366
+ },
1367
+ tool(async () => sweepExpired(client))
1368
+ );
1369
+ server.registerResource(
1370
+ "instances",
1371
+ "autodl://instances",
1372
+ {
1373
+ title: "AutoDL \u5B9E\u4F8B\u5217\u8868",
1374
+ description: "\u5F53\u524D\u8D26\u53F7\u4E0B\u6240\u6709\u5B9E\u4F8B\u7684\u5B9E\u65F6\u5FEB\u7167\uFF08JSON\uFF09\u3002",
1375
+ mimeType: "application/json"
1376
+ },
1377
+ async (uri) => {
1378
+ const instances = await listAllInstances(client);
1379
+ return {
1380
+ contents: [
1381
+ { uri: uri.href, mimeType: "application/json", text: JSON.stringify(instances, null, 2) }
1382
+ ]
1383
+ };
1384
+ }
1385
+ );
1386
+ return server;
1387
+ }
1388
+ async function startMcpServer(options = {}) {
1389
+ const context = createContext({
1390
+ ...options.token ? { token: options.token } : {},
1391
+ ...options.baseUrl ? { baseUrl: options.baseUrl } : {},
1392
+ json: true,
1393
+ sweep: false
1394
+ });
1395
+ const server = buildServer(context);
1396
+ const transport = new StdioServerTransport();
1397
+ await server.connect(transport);
1398
+ }
1399
+
1400
+ // src/ssh/connect.ts
1401
+ import { spawn } from "child_process";
1402
+ async function connectInteractive(client, uuid, options = {}) {
1403
+ const creds = await getCredentials(client, uuid, options);
1404
+ const args = [
1405
+ "-p",
1406
+ String(creds.port),
1407
+ // The proxy hosts recycle host keys across instances, so strict checking would
1408
+ // fail on every new rental and train users to ignore the warning.
1409
+ "-o",
1410
+ "StrictHostKeyChecking=no",
1411
+ "-o",
1412
+ "UserKnownHostsFile=/dev/null",
1413
+ "-o",
1414
+ "LogLevel=ERROR",
1415
+ ...options.extraArgs ?? [],
1416
+ `${creds.user}@${creds.host}`
1417
+ ];
1418
+ note(`ssh -p ${creds.port} ${creds.user}@${creds.host}`);
1419
+ note(`\u5BC6\u7801\uFF1A${creds.password}`);
1420
+ return new Promise((resolve, reject) => {
1421
+ const child = spawn("ssh", args, { stdio: "inherit" });
1422
+ child.on(
1423
+ "error",
1424
+ (err) => reject(
1425
+ new SSHError(`\u65E0\u6CD5\u542F\u52A8 ssh \u5BA2\u6237\u7AEF\uFF1A${err.message}`, {
1426
+ hint: "\u786E\u8BA4\u7CFB\u7EDF\u5DF2\u5B89\u88C5 OpenSSH \u5BA2\u6237\u7AEF\uFF08which ssh\uFF09\u3002",
1427
+ cause: err
1428
+ })
1429
+ )
1430
+ );
1431
+ child.on("close", (code) => resolve(code ?? 0));
1432
+ });
1433
+ }
1434
+ function formatSSHCommand(creds) {
1435
+ return `ssh -p ${creds.port} ${creds.user}@${creds.host}`;
1436
+ }
1437
+
1438
+ export {
1439
+ saveImage,
1440
+ listPrivateImages,
1441
+ parseRepo,
1442
+ withCredentials,
1443
+ redactCredentials,
1444
+ resolveGitToken,
1445
+ resolveMinBalance,
1446
+ assertBudget,
1447
+ parseUtilisation,
1448
+ watchIdle,
1449
+ push,
1450
+ pull,
1451
+ deployWorkflow,
1452
+ runWorkflow,
1453
+ buildServer,
1454
+ startMcpServer,
1455
+ connectInteractive,
1456
+ formatSSHCommand
1457
+ };
1458
+ //# sourceMappingURL=chunk-5RZ4Q7T4.js.map