@vellumai/cli 0.5.14 → 0.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/teleport.test.ts +568 -397
- package/src/commands/hatch.ts +3 -387
- package/src/commands/retire.ts +2 -120
- package/src/commands/teleport.ts +595 -187
- package/src/commands/wake.ts +29 -4
- package/src/lib/hatch-local.ts +403 -0
- package/src/lib/local.ts +9 -120
- package/src/lib/retire-local.ts +124 -0
package/src/commands/teleport.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
findAssistantByName,
|
|
3
|
+
loadAllAssistants,
|
|
4
|
+
removeAssistantEntry,
|
|
5
|
+
saveAssistantEntry,
|
|
6
|
+
setActiveAssistant,
|
|
7
|
+
} from "../lib/assistant-config.js";
|
|
2
8
|
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
3
9
|
import {
|
|
4
10
|
loadGuardianToken,
|
|
@@ -7,47 +13,143 @@ import {
|
|
|
7
13
|
import {
|
|
8
14
|
readPlatformToken,
|
|
9
15
|
fetchOrganizationId,
|
|
16
|
+
getPlatformUrl,
|
|
17
|
+
hatchAssistant,
|
|
10
18
|
platformInitiateExport,
|
|
11
19
|
platformPollExportStatus,
|
|
12
20
|
platformDownloadExport,
|
|
13
21
|
platformImportPreflight,
|
|
14
22
|
platformImportBundle,
|
|
15
23
|
} from "../lib/platform-client.js";
|
|
24
|
+
import {
|
|
25
|
+
hatchDocker,
|
|
26
|
+
retireDocker,
|
|
27
|
+
sleepContainers,
|
|
28
|
+
dockerResourceNames,
|
|
29
|
+
} from "../lib/docker.js";
|
|
30
|
+
import { hatchLocal } from "../lib/hatch-local.js";
|
|
31
|
+
import { retireLocal } from "../lib/retire-local.js";
|
|
32
|
+
import { validateAssistantName } from "../lib/retire-archive.js";
|
|
33
|
+
import { stopProcessByPidFile } from "../lib/process.js";
|
|
34
|
+
import { join } from "node:path";
|
|
16
35
|
|
|
17
36
|
function printHelp(): void {
|
|
18
37
|
console.log(
|
|
19
|
-
"Usage: vellum teleport --from <assistant> --
|
|
38
|
+
"Usage: vellum teleport --from <assistant> <--local | --docker | --platform> [name] [options]",
|
|
39
|
+
);
|
|
40
|
+
console.log("");
|
|
41
|
+
console.log(
|
|
42
|
+
"Transfer assistant data between local, docker, and platform environments.",
|
|
43
|
+
);
|
|
44
|
+
console.log("");
|
|
45
|
+
console.log(
|
|
46
|
+
"The --from flag specifies the source assistant to export data from.",
|
|
47
|
+
);
|
|
48
|
+
console.log(
|
|
49
|
+
"Exactly one environment flag (--local, --docker, --platform) specifies",
|
|
50
|
+
);
|
|
51
|
+
console.log(
|
|
52
|
+
"the target environment. An optional name after the environment flag",
|
|
53
|
+
);
|
|
54
|
+
console.log(
|
|
55
|
+
"targets an existing assistant (overwriting its data) or names a newly",
|
|
20
56
|
);
|
|
57
|
+
console.log(
|
|
58
|
+
"hatched one. If no name is given, a new assistant is hatched with an",
|
|
59
|
+
);
|
|
60
|
+
console.log("auto-generated name.");
|
|
21
61
|
console.log("");
|
|
22
62
|
console.log(
|
|
23
|
-
"
|
|
63
|
+
"The source and target must be different environments. Same-environment",
|
|
24
64
|
);
|
|
65
|
+
console.log("transfers (e.g. local to local) are not supported.");
|
|
66
|
+
console.log("");
|
|
67
|
+
console.log(
|
|
68
|
+
"For local-to-docker and docker-to-local transfers, the source assistant",
|
|
69
|
+
);
|
|
70
|
+
console.log(
|
|
71
|
+
"is automatically retired after a successful import to free up ports and",
|
|
72
|
+
);
|
|
73
|
+
console.log("avoid resource conflicts. Use --keep-source to skip this.");
|
|
74
|
+
console.log("");
|
|
75
|
+
console.log("Environment flags:");
|
|
76
|
+
console.log(" --local [name] Target a local bare-metal assistant");
|
|
77
|
+
console.log(" --docker [name] Target a docker assistant");
|
|
78
|
+
console.log(" --platform [name] Target a platform-hosted assistant");
|
|
25
79
|
console.log("");
|
|
26
80
|
console.log("Options:");
|
|
27
|
-
console.log(" --from <name> Source assistant to export data from");
|
|
28
|
-
console.log(" --to <name> Target assistant to import data into");
|
|
29
81
|
console.log(
|
|
30
|
-
" --
|
|
82
|
+
" --from <name> Source assistant to export data from (required)",
|
|
31
83
|
);
|
|
32
|
-
console.log(
|
|
84
|
+
console.log(
|
|
85
|
+
" --keep-source Do not retire the source after local/docker transfers",
|
|
86
|
+
);
|
|
87
|
+
console.log(
|
|
88
|
+
" --dry-run Preview the transfer without applying changes.",
|
|
89
|
+
);
|
|
90
|
+
console.log(
|
|
91
|
+
" If the target exists, runs preflight analysis.",
|
|
92
|
+
);
|
|
93
|
+
console.log(
|
|
94
|
+
" If the target would be hatched, shows what would happen",
|
|
95
|
+
);
|
|
96
|
+
console.log(" without creating anything.");
|
|
97
|
+
console.log(" --help, -h Show this help");
|
|
33
98
|
console.log("");
|
|
34
99
|
console.log("Examples:");
|
|
35
|
-
console.log(" vellum teleport --from my-local --
|
|
36
|
-
console.log(
|
|
37
|
-
|
|
100
|
+
console.log(" vellum teleport --from my-local --docker");
|
|
101
|
+
console.log(
|
|
102
|
+
" Hatch a new docker assistant, import data, and retire my-local",
|
|
103
|
+
);
|
|
104
|
+
console.log("");
|
|
105
|
+
console.log(" vellum teleport --from my-local --docker my-docker");
|
|
106
|
+
console.log(
|
|
107
|
+
" Import data from my-local into existing docker assistant my-docker",
|
|
108
|
+
);
|
|
109
|
+
console.log(
|
|
110
|
+
" (or hatch a new docker assistant named my-docker if it doesn't exist)",
|
|
111
|
+
);
|
|
112
|
+
console.log("");
|
|
113
|
+
console.log(" vellum teleport --from my-local --platform");
|
|
114
|
+
console.log(
|
|
115
|
+
" Hatch a new platform assistant and import data from my-local",
|
|
116
|
+
);
|
|
117
|
+
console.log("");
|
|
118
|
+
console.log(" vellum teleport --from my-cloud --local my-new-local");
|
|
119
|
+
console.log(
|
|
120
|
+
" Import data from platform assistant my-cloud into local assistant",
|
|
121
|
+
);
|
|
122
|
+
console.log("");
|
|
123
|
+
console.log(" vellum teleport --from my-docker --local --keep-source");
|
|
124
|
+
console.log(
|
|
125
|
+
" Transfer to a new local assistant but keep the docker source running",
|
|
126
|
+
);
|
|
127
|
+
console.log("");
|
|
128
|
+
console.log(
|
|
129
|
+
" vellum teleport --from staging --docker staging-copy --dry-run",
|
|
130
|
+
);
|
|
131
|
+
console.log(" Preview what would be imported without applying changes");
|
|
38
132
|
}
|
|
39
133
|
|
|
40
|
-
function parseArgs(argv: string[]): {
|
|
134
|
+
export function parseArgs(argv: string[]): {
|
|
41
135
|
from: string | undefined;
|
|
42
136
|
to: string | undefined;
|
|
137
|
+
targetEnv: "local" | "docker" | "platform" | undefined;
|
|
138
|
+
targetName: string | undefined;
|
|
139
|
+
keepSource: boolean;
|
|
43
140
|
dryRun: boolean;
|
|
44
141
|
help: boolean;
|
|
45
142
|
} {
|
|
46
143
|
let from: string | undefined;
|
|
47
144
|
let to: string | undefined;
|
|
145
|
+
let targetEnv: "local" | "docker" | "platform" | undefined;
|
|
146
|
+
let targetName: string | undefined;
|
|
147
|
+
let keepSource = false;
|
|
48
148
|
let dryRun = false;
|
|
49
149
|
let help = false;
|
|
50
150
|
|
|
151
|
+
const envFlags: string[] = [];
|
|
152
|
+
|
|
51
153
|
for (let i = 0; i < argv.length; i++) {
|
|
52
154
|
const arg = argv[i];
|
|
53
155
|
if (arg === "--from" && i + 1 < argv.length) {
|
|
@@ -60,6 +162,20 @@ function parseArgs(argv: string[]): {
|
|
|
60
162
|
continue;
|
|
61
163
|
}
|
|
62
164
|
to = argv[++i];
|
|
165
|
+
} else if (
|
|
166
|
+
arg === "--local" ||
|
|
167
|
+
arg === "--docker" ||
|
|
168
|
+
arg === "--platform"
|
|
169
|
+
) {
|
|
170
|
+
const env = arg.slice(2) as "local" | "docker" | "platform";
|
|
171
|
+
envFlags.push(env);
|
|
172
|
+
targetEnv = env;
|
|
173
|
+
// Peek at next arg for optional target name
|
|
174
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
|
|
175
|
+
targetName = argv[++i];
|
|
176
|
+
}
|
|
177
|
+
} else if (arg === "--keep-source") {
|
|
178
|
+
keepSource = true;
|
|
63
179
|
} else if (arg === "--dry-run") {
|
|
64
180
|
dryRun = true;
|
|
65
181
|
} else if (arg === "--help" || arg === "-h") {
|
|
@@ -67,7 +183,14 @@ function parseArgs(argv: string[]): {
|
|
|
67
183
|
}
|
|
68
184
|
}
|
|
69
185
|
|
|
70
|
-
|
|
186
|
+
if (envFlags.length > 1) {
|
|
187
|
+
console.error(
|
|
188
|
+
"Error: Only one environment flag (--local, --docker, --platform) may be specified.",
|
|
189
|
+
);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { from, to, targetEnv, targetName, keepSource, dryRun, help };
|
|
71
194
|
}
|
|
72
195
|
|
|
73
196
|
function resolveCloud(entry: AssistantEntry): string {
|
|
@@ -107,6 +230,245 @@ async function getAccessToken(
|
|
|
107
230
|
}
|
|
108
231
|
}
|
|
109
232
|
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// HTTP-based export/import helpers (shared by local and docker)
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
async function exportViaHttp(
|
|
238
|
+
entry: AssistantEntry,
|
|
239
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
240
|
+
let accessToken = await getAccessToken(
|
|
241
|
+
entry.runtimeUrl,
|
|
242
|
+
entry.assistantId,
|
|
243
|
+
entry.assistantId,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
let response: Response;
|
|
247
|
+
try {
|
|
248
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/export`, {
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers: {
|
|
251
|
+
Authorization: `Bearer ${accessToken}`,
|
|
252
|
+
"Content-Type": "application/json",
|
|
253
|
+
},
|
|
254
|
+
body: JSON.stringify({ description: "teleport export" }),
|
|
255
|
+
signal: AbortSignal.timeout(120_000),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Retry once with a fresh token on 401
|
|
259
|
+
if (response.status === 401) {
|
|
260
|
+
let refreshedToken: string | null = null;
|
|
261
|
+
try {
|
|
262
|
+
const freshToken = await leaseGuardianToken(
|
|
263
|
+
entry.runtimeUrl,
|
|
264
|
+
entry.assistantId,
|
|
265
|
+
);
|
|
266
|
+
refreshedToken = freshToken.accessToken;
|
|
267
|
+
} catch {
|
|
268
|
+
// If token refresh fails, fall through to the error handler below
|
|
269
|
+
}
|
|
270
|
+
if (refreshedToken) {
|
|
271
|
+
accessToken = refreshedToken;
|
|
272
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/export`, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: {
|
|
275
|
+
Authorization: `Bearer ${accessToken}`,
|
|
276
|
+
"Content-Type": "application/json",
|
|
277
|
+
},
|
|
278
|
+
body: JSON.stringify({ description: "teleport export" }),
|
|
279
|
+
signal: AbortSignal.timeout(120_000),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
285
|
+
console.error("Error: Export request timed out after 2 minutes.");
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
289
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
290
|
+
console.error(
|
|
291
|
+
`Error: Could not connect to assistant '${entry.assistantId}'. Is it running?`,
|
|
292
|
+
);
|
|
293
|
+
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
throw err;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (response.status === 401 || response.status === 403) {
|
|
300
|
+
console.error("Authentication failed.");
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (response.status === 404) {
|
|
305
|
+
console.error("Assistant not found or not running.");
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (
|
|
310
|
+
response.status === 502 ||
|
|
311
|
+
response.status === 503 ||
|
|
312
|
+
response.status === 504
|
|
313
|
+
) {
|
|
314
|
+
console.error(
|
|
315
|
+
`Assistant is unreachable. Try 'vellum wake ${entry.assistantId}'.`,
|
|
316
|
+
);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!response.ok) {
|
|
321
|
+
const body = await response.text();
|
|
322
|
+
console.error(`Error: Export failed (${response.status}): ${body}`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
327
|
+
return new Uint8Array(arrayBuffer);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function importViaHttp(
|
|
331
|
+
entry: AssistantEntry,
|
|
332
|
+
bundleData: Uint8Array<ArrayBuffer>,
|
|
333
|
+
dryRun: boolean,
|
|
334
|
+
): Promise<void> {
|
|
335
|
+
let accessToken = await getAccessToken(
|
|
336
|
+
entry.runtimeUrl,
|
|
337
|
+
entry.assistantId,
|
|
338
|
+
entry.assistantId,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
if (dryRun) {
|
|
342
|
+
console.log("Running preflight analysis...\n");
|
|
343
|
+
|
|
344
|
+
let response: Response;
|
|
345
|
+
try {
|
|
346
|
+
response = await fetch(
|
|
347
|
+
`${entry.runtimeUrl}/v1/migrations/import-preflight`,
|
|
348
|
+
{
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers: {
|
|
351
|
+
Authorization: `Bearer ${accessToken}`,
|
|
352
|
+
"Content-Type": "application/octet-stream",
|
|
353
|
+
},
|
|
354
|
+
body: new Blob([bundleData]),
|
|
355
|
+
signal: AbortSignal.timeout(120_000),
|
|
356
|
+
},
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
// Retry once with a fresh token on 401
|
|
360
|
+
if (response.status === 401) {
|
|
361
|
+
let refreshedToken: string | null = null;
|
|
362
|
+
try {
|
|
363
|
+
const freshToken = await leaseGuardianToken(
|
|
364
|
+
entry.runtimeUrl,
|
|
365
|
+
entry.assistantId,
|
|
366
|
+
);
|
|
367
|
+
refreshedToken = freshToken.accessToken;
|
|
368
|
+
} catch {
|
|
369
|
+
// If token refresh fails, fall through to the error handler below
|
|
370
|
+
}
|
|
371
|
+
if (refreshedToken) {
|
|
372
|
+
accessToken = refreshedToken;
|
|
373
|
+
response = await fetch(
|
|
374
|
+
`${entry.runtimeUrl}/v1/migrations/import-preflight`,
|
|
375
|
+
{
|
|
376
|
+
method: "POST",
|
|
377
|
+
headers: {
|
|
378
|
+
Authorization: `Bearer ${accessToken}`,
|
|
379
|
+
"Content-Type": "application/octet-stream",
|
|
380
|
+
},
|
|
381
|
+
body: new Blob([bundleData]),
|
|
382
|
+
signal: AbortSignal.timeout(120_000),
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
} catch (err) {
|
|
388
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
389
|
+
console.error("Error: Preflight request timed out after 2 minutes.");
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
393
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
394
|
+
console.error(
|
|
395
|
+
`Error: Could not connect to assistant '${entry.assistantId}'. Is it running?`,
|
|
396
|
+
);
|
|
397
|
+
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
handleLocalResponseErrors(response, entry.assistantId);
|
|
404
|
+
|
|
405
|
+
const result = (await response.json()) as PreflightResponse;
|
|
406
|
+
printPreflightSummary(result);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Actual import
|
|
411
|
+
console.log("Importing data...");
|
|
412
|
+
|
|
413
|
+
let response: Response;
|
|
414
|
+
try {
|
|
415
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/import`, {
|
|
416
|
+
method: "POST",
|
|
417
|
+
headers: {
|
|
418
|
+
Authorization: `Bearer ${accessToken}`,
|
|
419
|
+
"Content-Type": "application/octet-stream",
|
|
420
|
+
},
|
|
421
|
+
body: new Blob([bundleData]),
|
|
422
|
+
signal: AbortSignal.timeout(120_000),
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Retry once with a fresh token on 401
|
|
426
|
+
if (response.status === 401) {
|
|
427
|
+
let refreshedToken: string | null = null;
|
|
428
|
+
try {
|
|
429
|
+
const freshToken = await leaseGuardianToken(
|
|
430
|
+
entry.runtimeUrl,
|
|
431
|
+
entry.assistantId,
|
|
432
|
+
);
|
|
433
|
+
refreshedToken = freshToken.accessToken;
|
|
434
|
+
} catch {
|
|
435
|
+
// If token refresh fails, fall through to the error handler below
|
|
436
|
+
}
|
|
437
|
+
if (refreshedToken) {
|
|
438
|
+
accessToken = refreshedToken;
|
|
439
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/import`, {
|
|
440
|
+
method: "POST",
|
|
441
|
+
headers: {
|
|
442
|
+
Authorization: `Bearer ${accessToken}`,
|
|
443
|
+
"Content-Type": "application/octet-stream",
|
|
444
|
+
},
|
|
445
|
+
body: new Blob([bundleData]),
|
|
446
|
+
signal: AbortSignal.timeout(120_000),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} catch (err) {
|
|
451
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
452
|
+
console.error("Error: Import request timed out after 2 minutes.");
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
456
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
457
|
+
console.error(
|
|
458
|
+
`Error: Could not connect to assistant '${entry.assistantId}'. Is it running?`,
|
|
459
|
+
);
|
|
460
|
+
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
throw err;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
handleLocalResponseErrors(response, entry.assistantId);
|
|
467
|
+
|
|
468
|
+
const result = (await response.json()) as ImportResponse;
|
|
469
|
+
printImportSummary(result);
|
|
470
|
+
}
|
|
471
|
+
|
|
110
472
|
// ---------------------------------------------------------------------------
|
|
111
473
|
// Export from source assistant
|
|
112
474
|
// ---------------------------------------------------------------------------
|
|
@@ -205,100 +567,12 @@ async function exportFromAssistant(
|
|
|
205
567
|
return new Uint8Array(arrayBuffer);
|
|
206
568
|
}
|
|
207
569
|
|
|
208
|
-
if (cloud === "local") {
|
|
209
|
-
|
|
210
|
-
let accessToken = await getAccessToken(
|
|
211
|
-
entry.runtimeUrl,
|
|
212
|
-
entry.assistantId,
|
|
213
|
-
entry.assistantId,
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
let response: Response;
|
|
217
|
-
try {
|
|
218
|
-
response = await fetch(`${entry.runtimeUrl}/v1/migrations/export`, {
|
|
219
|
-
method: "POST",
|
|
220
|
-
headers: {
|
|
221
|
-
Authorization: `Bearer ${accessToken}`,
|
|
222
|
-
"Content-Type": "application/json",
|
|
223
|
-
},
|
|
224
|
-
body: JSON.stringify({ description: "teleport export" }),
|
|
225
|
-
signal: AbortSignal.timeout(120_000),
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// Retry once with a fresh token on 401
|
|
229
|
-
if (response.status === 401) {
|
|
230
|
-
let refreshedToken: string | null = null;
|
|
231
|
-
try {
|
|
232
|
-
const freshToken = await leaseGuardianToken(
|
|
233
|
-
entry.runtimeUrl,
|
|
234
|
-
entry.assistantId,
|
|
235
|
-
);
|
|
236
|
-
refreshedToken = freshToken.accessToken;
|
|
237
|
-
} catch {
|
|
238
|
-
// If token refresh fails, fall through to the error handler below
|
|
239
|
-
}
|
|
240
|
-
if (refreshedToken) {
|
|
241
|
-
accessToken = refreshedToken;
|
|
242
|
-
response = await fetch(`${entry.runtimeUrl}/v1/migrations/export`, {
|
|
243
|
-
method: "POST",
|
|
244
|
-
headers: {
|
|
245
|
-
Authorization: `Bearer ${accessToken}`,
|
|
246
|
-
"Content-Type": "application/json",
|
|
247
|
-
},
|
|
248
|
-
body: JSON.stringify({ description: "teleport export" }),
|
|
249
|
-
signal: AbortSignal.timeout(120_000),
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
} catch (err) {
|
|
254
|
-
if (err instanceof Error && err.name === "TimeoutError") {
|
|
255
|
-
console.error("Error: Export request timed out after 2 minutes.");
|
|
256
|
-
process.exit(1);
|
|
257
|
-
}
|
|
258
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
259
|
-
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
260
|
-
console.error(
|
|
261
|
-
`Error: Could not connect to assistant '${entry.assistantId}'. Is it running?`,
|
|
262
|
-
);
|
|
263
|
-
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
264
|
-
process.exit(1);
|
|
265
|
-
}
|
|
266
|
-
throw err;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (response.status === 401 || response.status === 403) {
|
|
270
|
-
console.error("Authentication failed.");
|
|
271
|
-
process.exit(1);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (response.status === 404) {
|
|
275
|
-
console.error("Assistant not found or not running.");
|
|
276
|
-
process.exit(1);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
response.status === 502 ||
|
|
281
|
-
response.status === 503 ||
|
|
282
|
-
response.status === 504
|
|
283
|
-
) {
|
|
284
|
-
console.error(
|
|
285
|
-
`Assistant is unreachable. Try 'vellum wake ${entry.assistantId}'.`,
|
|
286
|
-
);
|
|
287
|
-
process.exit(1);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (!response.ok) {
|
|
291
|
-
const body = await response.text();
|
|
292
|
-
console.error(`Error: Export failed (${response.status}): ${body}`);
|
|
293
|
-
process.exit(1);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
297
|
-
return new Uint8Array(arrayBuffer);
|
|
570
|
+
if (cloud === "local" || cloud === "docker") {
|
|
571
|
+
return exportViaHttp(entry);
|
|
298
572
|
}
|
|
299
573
|
|
|
300
574
|
console.error(
|
|
301
|
-
"Teleport only supports local and platform assistants as source.",
|
|
575
|
+
"Teleport only supports local, docker, and platform assistants as source.",
|
|
302
576
|
);
|
|
303
577
|
process.exit(1);
|
|
304
578
|
}
|
|
@@ -459,94 +733,118 @@ async function importToAssistant(
|
|
|
459
733
|
return;
|
|
460
734
|
}
|
|
461
735
|
|
|
462
|
-
if (cloud === "local") {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
entry.assistantId,
|
|
467
|
-
entry.assistantId,
|
|
468
|
-
);
|
|
736
|
+
if (cloud === "local" || cloud === "docker") {
|
|
737
|
+
await importViaHttp(entry, bundleData, dryRun);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
469
740
|
|
|
470
|
-
|
|
471
|
-
|
|
741
|
+
console.error(
|
|
742
|
+
"Teleport only supports local, docker, and platform assistants as target.",
|
|
743
|
+
);
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
472
746
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
747
|
+
// ---------------------------------------------------------------------------
|
|
748
|
+
// Resolve or hatch target assistant
|
|
749
|
+
// ---------------------------------------------------------------------------
|
|
750
|
+
|
|
751
|
+
export async function resolveOrHatchTarget(
|
|
752
|
+
targetEnv: "local" | "docker" | "platform",
|
|
753
|
+
targetName?: string,
|
|
754
|
+
): Promise<AssistantEntry> {
|
|
755
|
+
// If a name is provided, try to find an existing assistant
|
|
756
|
+
if (targetName) {
|
|
757
|
+
const existing = findAssistantByName(targetName);
|
|
758
|
+
if (existing) {
|
|
759
|
+
// Validate the existing assistant's cloud matches the requested env
|
|
760
|
+
const existingCloud = resolveCloud(existing);
|
|
761
|
+
const normalizedExisting =
|
|
762
|
+
existingCloud === "vellum" ? "platform" : existingCloud;
|
|
763
|
+
if (normalizedExisting !== targetEnv) {
|
|
764
|
+
console.error(
|
|
765
|
+
`Error: Assistant '${targetName}' is a ${normalizedExisting} assistant, not ${targetEnv}. ` +
|
|
766
|
+
`Use --${normalizedExisting} to target it.`,
|
|
486
767
|
);
|
|
487
|
-
|
|
488
|
-
if (err instanceof Error && err.name === "TimeoutError") {
|
|
489
|
-
console.error("Error: Preflight request timed out after 2 minutes.");
|
|
490
|
-
process.exit(1);
|
|
491
|
-
}
|
|
492
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
493
|
-
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
494
|
-
console.error(
|
|
495
|
-
`Error: Could not connect to assistant '${entry.assistantId}'. Is it running?`,
|
|
496
|
-
);
|
|
497
|
-
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
498
|
-
process.exit(1);
|
|
499
|
-
}
|
|
500
|
-
throw err;
|
|
768
|
+
process.exit(1);
|
|
501
769
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const result = (await response.json()) as PreflightResponse;
|
|
506
|
-
printPreflightSummary(result);
|
|
507
|
-
return;
|
|
770
|
+
console.log(`Target: ${targetName} (${targetEnv})`);
|
|
771
|
+
return existing;
|
|
508
772
|
}
|
|
509
773
|
|
|
510
|
-
//
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
body: new Blob([bundleData]),
|
|
522
|
-
signal: AbortSignal.timeout(120_000),
|
|
523
|
-
});
|
|
524
|
-
} catch (err) {
|
|
525
|
-
if (err instanceof Error && err.name === "TimeoutError") {
|
|
526
|
-
console.error("Error: Import request timed out after 2 minutes.");
|
|
527
|
-
process.exit(1);
|
|
528
|
-
}
|
|
529
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
530
|
-
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
774
|
+
// Name not found — will hatch.
|
|
775
|
+
if (targetEnv === "platform") {
|
|
776
|
+
// Platform API doesn't accept custom names — warn and ignore
|
|
777
|
+
console.log(
|
|
778
|
+
`Note: Platform assistants receive a server-assigned ID. The name '${targetName}' will not be used.`,
|
|
779
|
+
);
|
|
780
|
+
} else {
|
|
781
|
+
// Validate the name before passing to hatch
|
|
782
|
+
try {
|
|
783
|
+
validateAssistantName(targetName);
|
|
784
|
+
} catch {
|
|
531
785
|
console.error(
|
|
532
|
-
|
|
786
|
+
"Error: Target name contains invalid characters (path separators or traversal segments are not allowed).",
|
|
533
787
|
);
|
|
534
|
-
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
535
788
|
process.exit(1);
|
|
536
789
|
}
|
|
537
|
-
throw err;
|
|
538
790
|
}
|
|
791
|
+
}
|
|
539
792
|
|
|
540
|
-
|
|
793
|
+
// Hatch a new assistant in the target environment
|
|
794
|
+
if (targetEnv === "local") {
|
|
795
|
+
const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
|
|
796
|
+
await hatchLocal("vellum", targetName ?? null, false, false, false, {});
|
|
797
|
+
const entry = targetName
|
|
798
|
+
? findAssistantByName(targetName)
|
|
799
|
+
: (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??
|
|
800
|
+
null);
|
|
801
|
+
if (!entry) {
|
|
802
|
+
console.error("Error: Could not find the newly hatched local assistant.");
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
console.log(`Hatched new local assistant: ${entry.assistantId}`);
|
|
806
|
+
return entry;
|
|
807
|
+
}
|
|
541
808
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
809
|
+
if (targetEnv === "docker") {
|
|
810
|
+
const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
|
|
811
|
+
await hatchDocker("vellum", false, targetName ?? null, false, {});
|
|
812
|
+
const entry = targetName
|
|
813
|
+
? findAssistantByName(targetName)
|
|
814
|
+
: (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??
|
|
815
|
+
null);
|
|
816
|
+
if (!entry) {
|
|
817
|
+
console.error(
|
|
818
|
+
"Error: Could not find the newly hatched docker assistant.",
|
|
819
|
+
);
|
|
820
|
+
process.exit(1);
|
|
821
|
+
}
|
|
822
|
+
console.log(`Hatched new docker assistant: ${entry.assistantId}`);
|
|
823
|
+
return entry;
|
|
545
824
|
}
|
|
546
825
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
826
|
+
if (targetEnv === "platform") {
|
|
827
|
+
const token = readPlatformToken();
|
|
828
|
+
if (!token) {
|
|
829
|
+
console.error("Not logged in. Run 'vellum login' first.");
|
|
830
|
+
process.exit(1);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const result = await hatchAssistant(token);
|
|
834
|
+
const entry: AssistantEntry = {
|
|
835
|
+
assistantId: result.id,
|
|
836
|
+
runtimeUrl: getPlatformUrl(),
|
|
837
|
+
cloud: "vellum",
|
|
838
|
+
species: "vellum",
|
|
839
|
+
hatchedAt: new Date().toISOString(),
|
|
840
|
+
};
|
|
841
|
+
saveAssistantEntry(entry);
|
|
842
|
+
setActiveAssistant(result.id);
|
|
843
|
+
console.log(`Hatched new platform assistant: ${result.id}`);
|
|
844
|
+
return entry;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
console.error(`Error: Unknown target environment '${targetEnv}'.`);
|
|
550
848
|
process.exit(1);
|
|
551
849
|
}
|
|
552
850
|
|
|
@@ -701,19 +999,36 @@ function printImportSummary(result: ImportResponse): void {
|
|
|
701
999
|
|
|
702
1000
|
export async function teleport(): Promise<void> {
|
|
703
1001
|
const args = process.argv.slice(3);
|
|
704
|
-
const { from, to, dryRun, help } =
|
|
1002
|
+
const { from, to, targetEnv, targetName, keepSource, dryRun, help } =
|
|
1003
|
+
parseArgs(args);
|
|
705
1004
|
|
|
706
1005
|
if (help) {
|
|
707
1006
|
printHelp();
|
|
708
1007
|
process.exit(0);
|
|
709
1008
|
}
|
|
710
1009
|
|
|
711
|
-
|
|
1010
|
+
// Legacy --to flag deprecation
|
|
1011
|
+
if (to) {
|
|
1012
|
+
console.error("Error: --to is deprecated. Use environment flags instead:");
|
|
1013
|
+
console.error(
|
|
1014
|
+
" vellum teleport --from <source> --local|--docker|--platform [name]",
|
|
1015
|
+
);
|
|
1016
|
+
console.error("");
|
|
1017
|
+
console.error("Run 'vellum teleport --help' for details.");
|
|
1018
|
+
process.exit(1);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (!from) {
|
|
1022
|
+
printHelp();
|
|
1023
|
+
process.exit(1);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (!targetEnv) {
|
|
712
1027
|
printHelp();
|
|
713
1028
|
process.exit(1);
|
|
714
1029
|
}
|
|
715
1030
|
|
|
716
|
-
// Look up
|
|
1031
|
+
// Look up source assistant
|
|
717
1032
|
const fromEntry = findAssistantByName(from);
|
|
718
1033
|
if (!fromEntry) {
|
|
719
1034
|
console.error(
|
|
@@ -722,25 +1037,118 @@ export async function teleport(): Promise<void> {
|
|
|
722
1037
|
process.exit(1);
|
|
723
1038
|
}
|
|
724
1039
|
|
|
725
|
-
const
|
|
726
|
-
|
|
1040
|
+
const fromCloud = resolveCloud(fromEntry);
|
|
1041
|
+
|
|
1042
|
+
// Early same-environment guard — compare source cloud against the CLI flag
|
|
1043
|
+
// BEFORE exporting or hatching, to avoid creating orphaned assistants.
|
|
1044
|
+
const normalizedSourceEnv = fromCloud === "vellum" ? "platform" : fromCloud;
|
|
1045
|
+
if (normalizedSourceEnv === targetEnv) {
|
|
727
1046
|
console.error(
|
|
728
|
-
`
|
|
1047
|
+
`Cannot teleport between two ${targetEnv} assistants. Teleport transfers data across different environments.`,
|
|
729
1048
|
);
|
|
730
1049
|
process.exit(1);
|
|
731
1050
|
}
|
|
732
1051
|
|
|
733
|
-
|
|
734
|
-
|
|
1052
|
+
// Dry-run without an existing target: skip export, hatch, and import —
|
|
1053
|
+
// just report what would happen.
|
|
1054
|
+
if (dryRun) {
|
|
1055
|
+
const existingTarget = targetName ? findAssistantByName(targetName) : null;
|
|
1056
|
+
|
|
1057
|
+
if (existingTarget) {
|
|
1058
|
+
// Target exists — validate cloud matches the flag, then run preflight
|
|
1059
|
+
const toCloud = resolveCloud(existingTarget);
|
|
1060
|
+
const normalizedTargetEnv = toCloud === "vellum" ? "platform" : toCloud;
|
|
1061
|
+
if (normalizedTargetEnv !== targetEnv) {
|
|
1062
|
+
console.error(
|
|
1063
|
+
`Error: Assistant '${targetName}' is a ${normalizedTargetEnv} assistant, not ${targetEnv}. ` +
|
|
1064
|
+
`Use --${normalizedTargetEnv} to target it.`,
|
|
1065
|
+
);
|
|
1066
|
+
process.exit(1);
|
|
1067
|
+
}
|
|
1068
|
+
if (normalizedSourceEnv === normalizedTargetEnv) {
|
|
1069
|
+
console.error(
|
|
1070
|
+
`Cannot teleport between two ${normalizedTargetEnv} assistants. Teleport transfers data across different environments.`,
|
|
1071
|
+
);
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
console.log(`Exporting from ${from} (${fromCloud})...`);
|
|
1076
|
+
const bundleData = await exportFromAssistant(fromEntry, fromCloud);
|
|
1077
|
+
console.log(`Importing to ${existingTarget.assistantId} (${toCloud})...`);
|
|
1078
|
+
await importToAssistant(existingTarget, toCloud, bundleData, true);
|
|
1079
|
+
} else {
|
|
1080
|
+
// No existing target — just describe what would happen
|
|
1081
|
+
console.log("Dry run summary:");
|
|
1082
|
+
console.log(` Would export data from: ${from} (${fromCloud})`);
|
|
1083
|
+
console.log(
|
|
1084
|
+
` Would hatch a new ${targetEnv} assistant${targetName ? ` named '${targetName}'` : ""}`,
|
|
1085
|
+
);
|
|
1086
|
+
console.log(` Would import data into the new assistant`);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
console.log(`Dry run complete — no changes were made.`);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
735
1092
|
|
|
736
1093
|
// Export from source
|
|
737
1094
|
console.log(`Exporting from ${from} (${fromCloud})...`);
|
|
738
1095
|
const bundleData = await exportFromAssistant(fromEntry, fromCloud);
|
|
739
1096
|
|
|
1097
|
+
// For local<->docker transfers, stop (sleep) the source to free up ports
|
|
1098
|
+
// before hatching the target. We do NOT retire yet — if hatch or import
|
|
1099
|
+
// fails, the user can recover by running `vellum wake <source>`.
|
|
1100
|
+
const sourceIsLocalOrDocker = fromCloud === "local" || fromCloud === "docker";
|
|
1101
|
+
const targetIsLocalOrDocker = targetEnv === "local" || targetEnv === "docker";
|
|
1102
|
+
if (sourceIsLocalOrDocker && targetIsLocalOrDocker && !keepSource) {
|
|
1103
|
+
console.log(`Stopping source assistant '${from}' to free ports...`);
|
|
1104
|
+
if (fromCloud === "docker") {
|
|
1105
|
+
const res = dockerResourceNames(fromEntry.assistantId);
|
|
1106
|
+
await sleepContainers(res);
|
|
1107
|
+
} else if (fromEntry.resources) {
|
|
1108
|
+
const pidFile = fromEntry.resources.pidFile;
|
|
1109
|
+
const vellumDir = join(fromEntry.resources.instanceDir, ".vellum");
|
|
1110
|
+
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
1111
|
+
await stopProcessByPidFile(pidFile, "assistant");
|
|
1112
|
+
await stopProcessByPidFile(gatewayPidFile, "gateway", undefined, 7000);
|
|
1113
|
+
}
|
|
1114
|
+
console.log(`Source assistant '${from}' stopped.`);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Resolve or hatch target (after source is stopped to avoid port conflicts)
|
|
1118
|
+
const toEntry = await resolveOrHatchTarget(targetEnv, targetName);
|
|
1119
|
+
const toCloud = resolveCloud(toEntry);
|
|
1120
|
+
|
|
1121
|
+
// Post-hatch same-environment safety net — uses resolved clouds in case
|
|
1122
|
+
// the resolved target cloud differs from the CLI flag (e.g., --docker
|
|
1123
|
+
// targeting a name that is actually a local entry).
|
|
1124
|
+
const normalizedTargetEnv = toCloud === "vellum" ? "platform" : toCloud;
|
|
1125
|
+
if (normalizedSourceEnv === normalizedTargetEnv) {
|
|
1126
|
+
console.error(
|
|
1127
|
+
`Cannot teleport between two ${normalizedTargetEnv} assistants. Teleport transfers data across different environments.`,
|
|
1128
|
+
);
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
740
1132
|
// Import to target
|
|
741
|
-
console.log(`Importing to ${
|
|
742
|
-
await importToAssistant(toEntry, toCloud, bundleData,
|
|
1133
|
+
console.log(`Importing to ${toEntry.assistantId} (${toCloud})...`);
|
|
1134
|
+
await importToAssistant(toEntry, toCloud, bundleData, false);
|
|
1135
|
+
|
|
1136
|
+
// Retire source after successful import
|
|
1137
|
+
if (sourceIsLocalOrDocker && targetIsLocalOrDocker) {
|
|
1138
|
+
if (!keepSource) {
|
|
1139
|
+
console.log(`Retiring source assistant '${from}'...`);
|
|
1140
|
+
if (fromCloud === "docker") {
|
|
1141
|
+
await retireDocker(fromEntry.assistantId);
|
|
1142
|
+
} else {
|
|
1143
|
+
await retireLocal(fromEntry.assistantId, fromEntry);
|
|
1144
|
+
}
|
|
1145
|
+
removeAssistantEntry(fromEntry.assistantId);
|
|
1146
|
+
console.log(`Source assistant '${from}' retired.`);
|
|
1147
|
+
} else {
|
|
1148
|
+
console.log(`Source assistant '${from}' kept (--keep-source).`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
743
1151
|
|
|
744
1152
|
// Success summary
|
|
745
|
-
console.log(`Teleport complete: ${from} → ${
|
|
1153
|
+
console.log(`Teleport complete: ${from} → ${toEntry.assistantId}`);
|
|
746
1154
|
}
|