@vellumai/cli 0.7.0 → 0.7.2
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/AGENTS.md +3 -11
- package/README.md +49 -0
- package/bun.lock +0 -15
- package/package.json +1 -6
- package/src/__tests__/backup.test.ts +591 -0
- package/src/__tests__/config-utils.test.ts +35 -48
- package/src/__tests__/teleport.test.ts +597 -37
- package/src/commands/backup.ts +149 -70
- package/src/commands/client.ts +56 -14
- package/src/commands/events.ts +3 -0
- package/src/commands/exec.ts +34 -12
- package/src/commands/hatch.ts +3 -7
- package/src/commands/login.ts +15 -33
- package/src/commands/logs.ts +2 -7
- package/src/commands/ps.ts +41 -6
- package/src/commands/restore.ts +32 -47
- package/src/commands/setup.ts +38 -73
- package/src/commands/ssh.ts +2 -5
- package/src/commands/teleport.ts +148 -34
- package/src/commands/tunnel.ts +2 -7
- package/src/commands/upgrade.ts +114 -7
- package/src/commands/wake.ts +5 -16
- package/src/components/DefaultMainScreen.tsx +65 -129
- package/src/index.ts +2 -13
- package/src/lib/__tests__/docker.test.ts +50 -32
- package/src/lib/__tests__/local-runtime-client.test.ts +308 -25
- package/src/lib/__tests__/platform-client-signed-url.test.ts +237 -2
- package/src/lib/__tests__/runtime-url.test.ts +125 -0
- package/src/lib/__tests__/terminal-session.test.ts +202 -0
- package/src/lib/assistant-client.ts +18 -26
- package/src/lib/assistant-config.ts +34 -41
- package/src/lib/backup-ops.ts +43 -17
- package/src/lib/cli-error.ts +1 -0
- package/src/lib/client-identity.ts +1 -1
- package/src/lib/config-utils.ts +1 -97
- package/src/lib/docker-statefulset.ts +381 -0
- package/src/lib/docker.ts +8 -247
- package/src/lib/guardian-token.ts +56 -6
- package/src/lib/hatch-local.ts +3 -26
- package/src/lib/job-polling.ts +1 -1
- package/src/lib/local-runtime-client.ts +162 -28
- package/src/lib/local.ts +35 -64
- package/src/lib/ngrok.ts +36 -26
- package/src/lib/platform-client.ts +97 -221
- package/src/lib/platform-releases.ts +23 -0
- package/src/lib/retire-local.ts +2 -2
- package/src/lib/runtime-url.ts +52 -0
- package/src/lib/sync-cloud-assistants.ts +126 -0
- package/src/lib/terminal-client.ts +6 -1
- package/src/lib/terminal-session.ts +127 -48
- package/src/lib/tui-log.ts +60 -0
- package/src/lib/upgrade-lifecycle.ts +65 -0
- package/src/lib/xdg-log.ts +10 -4
- package/src/commands/pair.ts +0 -212
package/src/commands/backup.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "fs";
|
|
2
2
|
import { dirname, join } from "path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
5
|
+
import { findAssistantByName } from "../lib/assistant-config.js";
|
|
5
6
|
import { getBackupsDir, formatSize } from "../lib/backup-ops.js";
|
|
6
7
|
import { loadGuardianToken, leaseGuardianToken } from "../lib/guardian-token";
|
|
8
|
+
import { pollJobUntilDone } from "../lib/job-polling.js";
|
|
7
9
|
import {
|
|
10
|
+
MigrationInProgressError,
|
|
11
|
+
localRuntimeExportToGcs,
|
|
12
|
+
localRuntimeIdentity,
|
|
13
|
+
localRuntimePollJobStatus,
|
|
14
|
+
} from "../lib/local-runtime-client.js";
|
|
15
|
+
import {
|
|
16
|
+
platformRequestSignedUrl,
|
|
8
17
|
readPlatformToken,
|
|
9
|
-
platformInitiateExport,
|
|
10
|
-
platformPollExportStatus,
|
|
11
|
-
platformDownloadExport,
|
|
12
18
|
} from "../lib/platform-client.js";
|
|
13
19
|
|
|
14
20
|
export async function backup(): Promise<void> {
|
|
@@ -73,7 +79,7 @@ export async function backup(): Promise<void> {
|
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
if (cloud === "vellum") {
|
|
76
|
-
await backupPlatform(name, outputArg
|
|
82
|
+
await backupPlatform(entry, name, outputArg);
|
|
77
83
|
return;
|
|
78
84
|
}
|
|
79
85
|
|
|
@@ -188,39 +194,87 @@ export async function backup(): Promise<void> {
|
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
// ---------------------------------------------------------------------------
|
|
191
|
-
// Platform (
|
|
197
|
+
// Platform-managed (cloud="vellum") backup over GCS.
|
|
198
|
+
//
|
|
199
|
+
// The runtime exports the bundle straight to a platform-issued signed GCS
|
|
200
|
+
// URL; the CLI then downloads from GCS to local disk. Bytes never flow
|
|
201
|
+
// through Django. Same architectural shape as the platform-source half of
|
|
202
|
+
// `vellum teleport`. Output format and success log lines match mode 1
|
|
203
|
+
// (runtime-direct local backup) so users see one consistent UX.
|
|
204
|
+
//
|
|
205
|
+
// Lifecycle: the GCS bucket has a 1-day TTL on `uploads/<org>/*` objects
|
|
206
|
+
// (see `vellum-assistant-platform/django/app/assistant/migration/views.py`
|
|
207
|
+
// and `migration/services.py`). Backup is single-shot with no import to
|
|
208
|
+
// trigger best-effort cleanup, so the bundle sits in GCS up to 24h before
|
|
209
|
+
// TTL deletion. No explicit cleanup endpoint exists; relying on TTL is
|
|
210
|
+
// intentional.
|
|
192
211
|
// ---------------------------------------------------------------------------
|
|
193
|
-
|
|
194
212
|
async function backupPlatform(
|
|
213
|
+
entry: AssistantEntry,
|
|
195
214
|
name: string,
|
|
196
215
|
outputArg?: string,
|
|
197
|
-
runtimeUrl?: string,
|
|
198
216
|
): Promise<void> {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
217
|
+
const platformToken = readPlatformToken();
|
|
218
|
+
if (!platformToken) {
|
|
219
|
+
console.error(
|
|
220
|
+
"Not logged in. Run 'vellum login' first (required for platform-managed backup).",
|
|
221
|
+
);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
// Pin upload, download, and runtime requests to the same platform instance
|
|
225
|
+
// the assistant lives on. Using `getPlatformUrl()` instead would target
|
|
226
|
+
// whatever the lockfile / env-var resolves to, which may differ from
|
|
227
|
+
// `entry.runtimeUrl` for staging/dev assistants and end up signing URLs
|
|
228
|
+
// for the wrong GCS bucket. Mirrors the teleport bundlePlatformUrl
|
|
229
|
+
// threading at `cli/src/commands/teleport.ts:1311-1312`.
|
|
230
|
+
const platformUrl = entry.runtimeUrl;
|
|
231
|
+
// Track the working platform token across kickoff/poll/download so a
|
|
232
|
+
// 401-driven refresh during polling stays consistent through the final
|
|
233
|
+
// signed-download request.
|
|
234
|
+
let exportPlatformToken = platformToken;
|
|
235
|
+
|
|
236
|
+
// Step 0 — Ask the source runtime which version it's running. The bundle
|
|
237
|
+
// is produced by the daemon (not the CLI), and the CLI version can drift
|
|
238
|
+
// from the daemon version, so the daemon's version is the authoritative
|
|
239
|
+
// value to record as the bundle's `min_runtime_version`. Stamping with
|
|
240
|
+
// `cliPkg.version` here would record an inaccurate compatibility band on
|
|
241
|
+
// the signed-URL request.
|
|
242
|
+
let runtimeIdentity: { version: string };
|
|
243
|
+
try {
|
|
244
|
+
runtimeIdentity = await localRuntimeIdentity(entry, exportPlatformToken);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
247
|
+
console.error(
|
|
248
|
+
`Error: Could not fetch runtime identity from '${name}': ${msg}`,
|
|
249
|
+
);
|
|
203
250
|
process.exit(1);
|
|
204
251
|
}
|
|
205
252
|
|
|
206
|
-
// Step
|
|
253
|
+
// Step 1 — Request a signed upload URL.
|
|
254
|
+
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
255
|
+
{
|
|
256
|
+
operation: "upload",
|
|
257
|
+
minRuntimeVersion: runtimeIdentity.version,
|
|
258
|
+
maxRuntimeVersion: null,
|
|
259
|
+
},
|
|
260
|
+
exportPlatformToken,
|
|
261
|
+
platformUrl,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Step 2 — Kick off runtime export-to-GCS through the platform's
|
|
265
|
+
// wildcard runtime proxy. `localRuntimeExportToGcs` builds the
|
|
266
|
+
// `/v1/assistants/<id>/migrations/export-to-gcs` URL for cloud="vellum"
|
|
267
|
+
// and uses platform-token auth (no guardian-token bootstrap).
|
|
207
268
|
let jobId: string;
|
|
208
269
|
try {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
"CLI backup",
|
|
212
|
-
|
|
213
|
-
);
|
|
214
|
-
jobId = result.jobId;
|
|
270
|
+
({ jobId } = await localRuntimeExportToGcs(entry, exportPlatformToken, {
|
|
271
|
+
uploadUrl,
|
|
272
|
+
description: "CLI backup",
|
|
273
|
+
}));
|
|
215
274
|
} catch (err) {
|
|
216
|
-
|
|
217
|
-
if (msg.includes("401") || msg.includes("403")) {
|
|
218
|
-
console.error("Authentication failed. Run 'vellum login' to refresh.");
|
|
219
|
-
process.exit(1);
|
|
220
|
-
}
|
|
221
|
-
if (msg.includes("429")) {
|
|
275
|
+
if (err instanceof MigrationInProgressError) {
|
|
222
276
|
console.error(
|
|
223
|
-
|
|
277
|
+
`Error: Another backup or teleport export is already in progress on '${entry.assistantId}' (job ${err.existingJobId}). Wait for it to finish, then re-run.`,
|
|
224
278
|
);
|
|
225
279
|
process.exit(1);
|
|
226
280
|
}
|
|
@@ -229,65 +283,90 @@ async function backupPlatform(
|
|
|
229
283
|
|
|
230
284
|
console.log(`Export started (job ${jobId})...`);
|
|
231
285
|
|
|
232
|
-
// Step 3 — Poll
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// Let non-transient errors (e.g. 404 "job not found") propagate immediately
|
|
245
|
-
if (msg.includes("not found")) {
|
|
246
|
-
throw err;
|
|
286
|
+
// Step 3 — Poll the job through the wildcard proxy. The dedicated
|
|
287
|
+
// `/v1/migrations/jobs/{id}/` endpoint queries platform-side ImportJob
|
|
288
|
+
// records and would 404 on runtime-created job IDs.
|
|
289
|
+
const terminal = await pollJobUntilDone({
|
|
290
|
+
label: "platform export",
|
|
291
|
+
poll: () => localRuntimePollJobStatus(entry, exportPlatformToken, jobId),
|
|
292
|
+
refreshOn401: async () => {
|
|
293
|
+
const refreshed = readPlatformToken();
|
|
294
|
+
if (!refreshed) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
"Platform auth expired during export and no credential was found on disk. Run 'vellum login' and retry.",
|
|
297
|
+
);
|
|
247
298
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
299
|
+
exportPlatformToken = refreshed;
|
|
300
|
+
},
|
|
301
|
+
});
|
|
252
302
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (status.status === "failed") {
|
|
259
|
-
console.error(`Export failed: ${status.error ?? "unknown error"}`);
|
|
260
|
-
process.exit(1);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Still in progress — wait and retry
|
|
264
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
303
|
+
if (terminal.status === "failed") {
|
|
304
|
+
console.error(`Error: Export failed: ${terminal.error}`);
|
|
305
|
+
process.exit(1);
|
|
265
306
|
}
|
|
266
307
|
|
|
267
|
-
|
|
268
|
-
|
|
308
|
+
// Step 4 — Request a signed download URL for the same bundle and fetch
|
|
309
|
+
// it from GCS directly. No auth on signed URLs.
|
|
310
|
+
// Use `exportPlatformToken` (not the original `platformToken`) so a
|
|
311
|
+
// poll-loop 401 refresh doesn't get clobbered here — otherwise a long
|
|
312
|
+
// export that recovered mid-poll via re-auth would still 401 on the
|
|
313
|
+
// download-URL request and abort an otherwise successful run.
|
|
314
|
+
//
|
|
315
|
+
// We deliberately do NOT send `targetRuntimeVersion` here. This flow
|
|
316
|
+
// saves the bundle to disk for offline storage; there is no target
|
|
317
|
+
// runtime to gate against, and the user can later restore the file
|
|
318
|
+
// into any compatible runtime. Sending the CLI's version would
|
|
319
|
+
// incorrectly block older CLIs from backing up newer assistants.
|
|
320
|
+
// The platform treats `target_runtime_version` as optional and skips
|
|
321
|
+
// the version check when it's omitted.
|
|
322
|
+
const { url: bundleUrl } = await platformRequestSignedUrl(
|
|
323
|
+
{
|
|
324
|
+
operation: "download",
|
|
325
|
+
bundleKey,
|
|
326
|
+
},
|
|
327
|
+
exportPlatformToken,
|
|
328
|
+
platformUrl,
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
let downloadResponse: Response;
|
|
332
|
+
try {
|
|
333
|
+
downloadResponse = await fetch(bundleUrl);
|
|
334
|
+
} catch (err) {
|
|
335
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
336
|
+
console.error(`Error: Failed to fetch bundle from GCS: ${msg}`);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
if (!downloadResponse.ok) {
|
|
340
|
+
const body = await downloadResponse.text().catch(() => "");
|
|
341
|
+
console.error(
|
|
342
|
+
`Error: Failed to fetch bundle from GCS (${downloadResponse.status}): ${body}`,
|
|
343
|
+
);
|
|
269
344
|
process.exit(1);
|
|
270
345
|
}
|
|
271
346
|
|
|
272
|
-
|
|
347
|
+
const arrayBuffer = await downloadResponse.arrayBuffer();
|
|
348
|
+
const data = new Uint8Array(arrayBuffer);
|
|
349
|
+
|
|
350
|
+
// Step 5 — Write to disk using the same path resolution mode 1 uses.
|
|
273
351
|
const isoTimestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
274
352
|
const outputPath =
|
|
275
353
|
outputArg || join(getBackupsDir(), `${name}-${isoTimestamp}.vbundle`);
|
|
276
|
-
|
|
277
354
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
278
|
-
|
|
279
|
-
const response = await platformDownloadExport(downloadUrl);
|
|
280
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
281
|
-
const data = new Uint8Array(arrayBuffer);
|
|
282
|
-
|
|
283
355
|
writeFileSync(outputPath, data);
|
|
284
356
|
|
|
285
|
-
// Step
|
|
357
|
+
// Step 6 — Print success. Manifest SHA is included only if the runtime
|
|
358
|
+
// surfaced it via the unified job result; the export-to-gcs runtime
|
|
359
|
+
// route does not set the legacy `X-Vbundle-Manifest-Sha256` response
|
|
360
|
+
// header.
|
|
286
361
|
console.log(`Backup saved to ${outputPath}`);
|
|
287
362
|
console.log(`Size: ${formatSize(data.byteLength)}`);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
363
|
+
const manifestSha =
|
|
364
|
+
terminal.status === "complete" &&
|
|
365
|
+
terminal.result &&
|
|
366
|
+
typeof terminal.result === "object"
|
|
367
|
+
? (terminal.result as Record<string, unknown>).manifest_sha256
|
|
368
|
+
: undefined;
|
|
369
|
+
if (typeof manifestSha === "string") {
|
|
291
370
|
console.log(`Manifest SHA-256: ${manifestSha}`);
|
|
292
371
|
}
|
|
293
372
|
}
|
package/src/commands/client.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { hostname } from "os";
|
|
|
3
3
|
import {
|
|
4
4
|
findAssistantByName,
|
|
5
5
|
getActiveAssistant,
|
|
6
|
-
|
|
6
|
+
resolveAssistant,
|
|
7
7
|
} from "../lib/assistant-config";
|
|
8
8
|
import {
|
|
9
9
|
DAEMON_INTERNAL_ASSISTANT_ID,
|
|
@@ -11,7 +11,12 @@ import {
|
|
|
11
11
|
type Species,
|
|
12
12
|
} from "../lib/constants";
|
|
13
13
|
import { loadGuardianToken } from "../lib/guardian-token";
|
|
14
|
-
import { getLocalLanIPv4
|
|
14
|
+
import { getLocalLanIPv4 } from "../lib/local";
|
|
15
|
+
import {
|
|
16
|
+
fetchOrganizationId,
|
|
17
|
+
readPlatformToken,
|
|
18
|
+
} from "../lib/platform-client";
|
|
19
|
+
import { tuiLog } from "../lib/tui-log";
|
|
15
20
|
|
|
16
21
|
const ANSI = {
|
|
17
22
|
reset: "\x1b[0m",
|
|
@@ -25,6 +30,11 @@ interface ParsedArgs {
|
|
|
25
30
|
runtimeUrl: string;
|
|
26
31
|
assistantId: string;
|
|
27
32
|
species: Species;
|
|
33
|
+
/** "vellum" for platform-hosted assistants, undefined for local. */
|
|
34
|
+
cloud?: string;
|
|
35
|
+
/** Platform session token (X-Session-Token), set when cloud === "vellum". */
|
|
36
|
+
platformToken?: string;
|
|
37
|
+
/** Guardian JWT (Authorization: Bearer), set for local assistants. */
|
|
28
38
|
bearerToken?: string;
|
|
29
39
|
project?: string;
|
|
30
40
|
zone?: string;
|
|
@@ -76,8 +86,8 @@ function parseArgs(): ParsedArgs {
|
|
|
76
86
|
}
|
|
77
87
|
}
|
|
78
88
|
if (!entry && hasExplicitUrl) {
|
|
79
|
-
// URL provided but active assistant missing or unset —
|
|
80
|
-
entry =
|
|
89
|
+
// URL provided but active assistant missing or unset — resolve for remaining defaults
|
|
90
|
+
entry = resolveAssistant();
|
|
81
91
|
} else if (!entry) {
|
|
82
92
|
console.error(
|
|
83
93
|
"No active assistant set. Set one with 'vellum use <name>' or specify a name: 'vellum client <name>'.",
|
|
@@ -88,10 +98,17 @@ function parseArgs(): ParsedArgs {
|
|
|
88
98
|
|
|
89
99
|
let runtimeUrl = entry?.localUrl || entry?.runtimeUrl || FALLBACK_RUNTIME_URL;
|
|
90
100
|
let assistantId = entry?.assistantId || DAEMON_INTERNAL_ASSISTANT_ID;
|
|
91
|
-
const
|
|
92
|
-
loadGuardianToken(entry?.assistantId ?? "")?.accessToken ?? undefined;
|
|
101
|
+
const cloud = entry?.cloud;
|
|
93
102
|
const species: Species = (entry?.species as Species) ?? "vellum";
|
|
94
103
|
|
|
104
|
+
// Platform-hosted assistants use a session token; local assistants use a guardian JWT.
|
|
105
|
+
const platformToken =
|
|
106
|
+
cloud === "vellum" ? (readPlatformToken() ?? undefined) : undefined;
|
|
107
|
+
const bearerToken =
|
|
108
|
+
cloud === "vellum"
|
|
109
|
+
? undefined
|
|
110
|
+
: (loadGuardianToken(entry?.assistantId ?? "")?.accessToken ?? undefined);
|
|
111
|
+
|
|
95
112
|
for (let i = 0; i < flagArgs.length; i++) {
|
|
96
113
|
const flag = flagArgs[i];
|
|
97
114
|
if ((flag === "--url" || flag === "-u") && flagArgs[i + 1]) {
|
|
@@ -108,6 +125,8 @@ function parseArgs(): ParsedArgs {
|
|
|
108
125
|
runtimeUrl: maybeSwapToLocalhost(runtimeUrl.replace(/\/+$/, "")),
|
|
109
126
|
assistantId,
|
|
110
127
|
species,
|
|
128
|
+
cloud,
|
|
129
|
+
platformToken,
|
|
111
130
|
bearerToken,
|
|
112
131
|
project: entry?.project,
|
|
113
132
|
zone: entry?.zone,
|
|
@@ -140,11 +159,6 @@ function maybeSwapToLocalhost(url: string): string {
|
|
|
140
159
|
}
|
|
141
160
|
}
|
|
142
161
|
|
|
143
|
-
const macHost = getMacLocalHostname();
|
|
144
|
-
if (macHost) {
|
|
145
|
-
localNames.push(macHost.toLowerCase());
|
|
146
|
-
}
|
|
147
|
-
|
|
148
162
|
const lanIp = getLocalLanIPv4();
|
|
149
163
|
if (lanIp) {
|
|
150
164
|
localNames.push(lanIp);
|
|
@@ -185,8 +199,34 @@ ${ANSI.bold}EXAMPLES:${ANSI.reset}
|
|
|
185
199
|
}
|
|
186
200
|
|
|
187
201
|
export async function client(): Promise<void> {
|
|
188
|
-
const {
|
|
189
|
-
|
|
202
|
+
const {
|
|
203
|
+
runtimeUrl,
|
|
204
|
+
assistantId,
|
|
205
|
+
species,
|
|
206
|
+
cloud,
|
|
207
|
+
platformToken,
|
|
208
|
+
bearerToken,
|
|
209
|
+
project,
|
|
210
|
+
zone,
|
|
211
|
+
} = parseArgs();
|
|
212
|
+
|
|
213
|
+
tuiLog.init();
|
|
214
|
+
tuiLog.info("session start", { runtimeUrl, assistantId, species, cloud });
|
|
215
|
+
|
|
216
|
+
// Build pre-constructed auth headers so all fetch sites share a single object.
|
|
217
|
+
let auth: Record<string, string> | undefined;
|
|
218
|
+
if (cloud === "vellum" && platformToken) {
|
|
219
|
+
const orgId = await fetchOrganizationId(platformToken).catch((err) => {
|
|
220
|
+
tuiLog.warn("failed to fetch organization id", { err: String(err) });
|
|
221
|
+
return undefined;
|
|
222
|
+
});
|
|
223
|
+
auth = {
|
|
224
|
+
"X-Session-Token": platformToken,
|
|
225
|
+
...(orgId ? { "Vellum-Organization-Id": orgId } : {}),
|
|
226
|
+
};
|
|
227
|
+
} else if (bearerToken) {
|
|
228
|
+
auth = { Authorization: `Bearer ${bearerToken}` };
|
|
229
|
+
}
|
|
190
230
|
|
|
191
231
|
const { renderChatApp } = await import("../components/DefaultMainScreen");
|
|
192
232
|
|
|
@@ -197,11 +237,13 @@ export async function client(): Promise<void> {
|
|
|
197
237
|
assistantId,
|
|
198
238
|
species,
|
|
199
239
|
() => {
|
|
240
|
+
tuiLog.info("session end (user disconnect)");
|
|
241
|
+
tuiLog.close();
|
|
200
242
|
app.unmount();
|
|
201
243
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
202
244
|
console.log(`${ANSI.dim}Disconnected.${ANSI.reset}`);
|
|
203
245
|
process.exit(0);
|
|
204
246
|
},
|
|
205
|
-
{
|
|
247
|
+
{ auth, project, zone },
|
|
206
248
|
);
|
|
207
249
|
}
|
package/src/commands/events.ts
CHANGED
package/src/commands/exec.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
findAssistantByName,
|
|
5
|
-
loadLatestAssistant,
|
|
6
|
-
resolveCloud,
|
|
7
|
-
} from "../lib/assistant-config";
|
|
3
|
+
import { resolveAssistant, resolveCloud } from "../lib/assistant-config";
|
|
8
4
|
import { dockerResourceNames } from "../lib/docker";
|
|
9
5
|
import type { ServiceName } from "../lib/docker";
|
|
10
6
|
import { execAppleContainer } from "../lib/exec-apple-container";
|
|
@@ -84,6 +80,9 @@ export async function exec(): Promise<void> {
|
|
|
84
80
|
console.log(
|
|
85
81
|
" -it Interactive mode with TTY (like docker exec -it)",
|
|
86
82
|
);
|
|
83
|
+
console.log(
|
|
84
|
+
" --timeout <secs> Timeout in seconds (default: 30, 0 = no timeout)",
|
|
85
|
+
);
|
|
87
86
|
console.log(
|
|
88
87
|
" --verbose Show debug output (SSE events, sentinel parsing)",
|
|
89
88
|
);
|
|
@@ -120,12 +119,20 @@ export async function exec(): Promise<void> {
|
|
|
120
119
|
let serviceRaw = "assistant";
|
|
121
120
|
let interactive = false;
|
|
122
121
|
let verbose = false;
|
|
122
|
+
let timeoutMs = 30_000;
|
|
123
123
|
|
|
124
124
|
for (let i = 0; i < preArgs.length; i++) {
|
|
125
125
|
if (preArgs[i] === "--service" && preArgs[i + 1]) {
|
|
126
126
|
serviceRaw = preArgs[++i];
|
|
127
127
|
} else if (preArgs[i] === "-it" || preArgs[i] === "-ti") {
|
|
128
128
|
interactive = true;
|
|
129
|
+
} else if (preArgs[i] === "--timeout" && preArgs[i + 1]) {
|
|
130
|
+
const secs = Number(preArgs[++i]);
|
|
131
|
+
if (!Number.isFinite(secs) || secs < 0) {
|
|
132
|
+
console.error("Error: --timeout must be a non-negative number.");
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
timeoutMs = secs === 0 ? 0 : secs * 1000;
|
|
129
136
|
} else if (preArgs[i] === "--verbose") {
|
|
130
137
|
verbose = true;
|
|
131
138
|
} else if (!preArgs[i].startsWith("-")) {
|
|
@@ -135,7 +142,7 @@ export async function exec(): Promise<void> {
|
|
|
135
142
|
|
|
136
143
|
const service = normalizeService(serviceRaw);
|
|
137
144
|
|
|
138
|
-
const entry =
|
|
145
|
+
const entry = resolveAssistant(nameArg);
|
|
139
146
|
|
|
140
147
|
if (!entry) {
|
|
141
148
|
if (nameArg) {
|
|
@@ -149,10 +156,19 @@ export async function exec(): Promise<void> {
|
|
|
149
156
|
const cloud = resolveCloud(entry);
|
|
150
157
|
|
|
151
158
|
if (cloud === "local") {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
const child = spawn(command[0], command.slice(1), { stdio: "inherit" });
|
|
160
|
+
await new Promise<void>((resolve) => {
|
|
161
|
+
child.on("close", (code) => {
|
|
162
|
+
process.exitCode = code ?? 0;
|
|
163
|
+
resolve();
|
|
164
|
+
});
|
|
165
|
+
child.on("error", (err) => {
|
|
166
|
+
console.error(`Error: ${err.message}`);
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
resolve();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
156
172
|
}
|
|
157
173
|
|
|
158
174
|
if (cloud === "apple-container") {
|
|
@@ -200,14 +216,20 @@ export async function exec(): Promise<void> {
|
|
|
200
216
|
platformUrl: getPlatformUrl(),
|
|
201
217
|
};
|
|
202
218
|
|
|
219
|
+
const serviceParam = service === "assistant" ? undefined : service;
|
|
220
|
+
|
|
203
221
|
if (interactive) {
|
|
204
222
|
// Interactive mode: shell-escape argv and delegate to full terminal
|
|
205
|
-
await interactiveSession(assistant, shellEscapeArgs(command));
|
|
223
|
+
await interactiveSession(assistant, shellEscapeArgs(command), serviceParam);
|
|
206
224
|
return;
|
|
207
225
|
}
|
|
208
226
|
|
|
209
227
|
// Non-interactive: sentinel-based output capture with exit code
|
|
210
|
-
await nonInteractiveExec(assistant, command, {
|
|
228
|
+
await nonInteractiveExec(assistant, command, {
|
|
229
|
+
verbose,
|
|
230
|
+
timeoutMs,
|
|
231
|
+
service: serviceParam,
|
|
232
|
+
});
|
|
211
233
|
return;
|
|
212
234
|
}
|
|
213
235
|
|
package/src/commands/hatch.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
VALID_SPECIES,
|
|
16
16
|
} from "../lib/constants";
|
|
17
17
|
import type { RemoteHost, Species } from "../lib/constants";
|
|
18
|
-
import {
|
|
18
|
+
import { buildNestedConfig } from "../lib/config-utils";
|
|
19
19
|
import { hatchDocker } from "../lib/docker";
|
|
20
20
|
import { hatchGcp } from "../lib/gcp";
|
|
21
21
|
import type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
@@ -32,7 +32,7 @@ export type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
|
32
32
|
const INSTALL_SCRIPT_REMOTE_PATH = "/tmp/vellum-install.sh";
|
|
33
33
|
|
|
34
34
|
const HATCH_TIMEOUT_MS: Record<Species, number> = {
|
|
35
|
-
vellum:
|
|
35
|
+
vellum: 5 * 60 * 1000,
|
|
36
36
|
openclaw: 10 * 60 * 1000,
|
|
37
37
|
};
|
|
38
38
|
const DEFAULT_SPECIES: Species = "vellum";
|
|
@@ -123,11 +123,7 @@ export async function buildStartupScript(
|
|
|
123
123
|
// and export the env var so the daemon reads it on first boot.
|
|
124
124
|
let configWriteBlock = "";
|
|
125
125
|
if (Object.keys(configValues).length > 0) {
|
|
126
|
-
const configJson = JSON.stringify(
|
|
127
|
-
buildInitialConfig(configValues),
|
|
128
|
-
null,
|
|
129
|
-
2,
|
|
130
|
-
);
|
|
126
|
+
const configJson = JSON.stringify(buildNestedConfig(configValues), null, 2);
|
|
131
127
|
configWriteBlock = `
|
|
132
128
|
echo "Writing default workspace config..."
|
|
133
129
|
VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH="/tmp/vellum-initial-config-$$.json"
|
package/src/commands/login.ts
CHANGED
|
@@ -3,12 +3,10 @@ import { spawn } from "child_process";
|
|
|
3
3
|
import { randomBytes } from "crypto";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
findAssistantByName,
|
|
7
6
|
getActiveAssistant,
|
|
8
|
-
|
|
7
|
+
resolveAssistant,
|
|
9
8
|
loadAllAssistants,
|
|
10
9
|
removeAssistantEntry,
|
|
11
|
-
saveAssistantEntry,
|
|
12
10
|
setActiveAssistant,
|
|
13
11
|
} from "../lib/assistant-config";
|
|
14
12
|
import { computeDeviceId } from "../lib/guardian-token";
|
|
@@ -26,6 +24,7 @@ import {
|
|
|
26
24
|
reprovisionAssistantApiKey,
|
|
27
25
|
savePlatformToken,
|
|
28
26
|
} from "../lib/platform-client";
|
|
27
|
+
import { syncCloudAssistants } from "../lib/sync-cloud-assistants";
|
|
29
28
|
|
|
30
29
|
const LOGIN_TIMEOUT_MS = 120_000; // 2 minutes
|
|
31
30
|
|
|
@@ -205,10 +204,7 @@ export async function login(): Promise<void> {
|
|
|
205
204
|
// Register the local assistant with the platform (non-fatal).
|
|
206
205
|
// Mirrors the desktop app's LocalAssistantBootstrapService flow.
|
|
207
206
|
try {
|
|
208
|
-
const
|
|
209
|
-
const entry = activeName
|
|
210
|
-
? findAssistantByName(activeName)
|
|
211
|
-
: loadLatestAssistant();
|
|
207
|
+
const entry = resolveAssistant();
|
|
212
208
|
|
|
213
209
|
// Skip managed ("vellum") assistants — they are handled by the platform.
|
|
214
210
|
if (entry && entry.cloud !== "vellum") {
|
|
@@ -282,36 +278,22 @@ export async function login(): Promise<void> {
|
|
|
282
278
|
// This ensures `vellum ps` shows managed assistants immediately
|
|
283
279
|
// after login (e.g. after a retire-and-rehatch cycle).
|
|
284
280
|
try {
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
let synced = 0;
|
|
293
|
-
for (const pa of platformAssistants) {
|
|
294
|
-
if (!existingIds.has(pa.id)) {
|
|
295
|
-
saveAssistantEntry({
|
|
296
|
-
assistantId: pa.id,
|
|
297
|
-
runtimeUrl: getPlatformUrl(),
|
|
298
|
-
cloud: "vellum",
|
|
299
|
-
species: "vellum",
|
|
300
|
-
hatchedAt: new Date().toISOString(),
|
|
301
|
-
});
|
|
302
|
-
synced++;
|
|
281
|
+
const result = await syncCloudAssistants();
|
|
282
|
+
if (result) {
|
|
283
|
+
const total = result.added + result.removed;
|
|
284
|
+
if (total > 0) {
|
|
285
|
+
console.log(
|
|
286
|
+
`Synced cloud assistants (${result.added} added, ${result.removed} removed).`,
|
|
287
|
+
);
|
|
303
288
|
}
|
|
304
289
|
}
|
|
305
290
|
|
|
306
|
-
if (synced > 0) {
|
|
307
|
-
console.log(
|
|
308
|
-
`Synced ${synced} cloud assistant${synced > 1 ? "s" : ""} to local lockfile.`,
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
291
|
// If no active assistant is set, activate the first cloud one.
|
|
313
|
-
if (!getActiveAssistant()
|
|
314
|
-
|
|
292
|
+
if (!getActiveAssistant()) {
|
|
293
|
+
const platformAssistants = await fetchPlatformAssistants(token);
|
|
294
|
+
if (platformAssistants.length > 0) {
|
|
295
|
+
setActiveAssistant(platformAssistants[0].id);
|
|
296
|
+
}
|
|
315
297
|
}
|
|
316
298
|
} catch {
|
|
317
299
|
// Non-fatal — login succeeded even if sync fails
|
package/src/commands/logs.ts
CHANGED
|
@@ -4,10 +4,7 @@ import { createInterface } from "readline";
|
|
|
4
4
|
import { watch } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
findAssistantByName,
|
|
9
|
-
loadLatestAssistant,
|
|
10
|
-
} from "../lib/assistant-config";
|
|
7
|
+
import { resolveAssistant } from "../lib/assistant-config";
|
|
11
8
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
12
9
|
import { dockerResourceNames } from "../lib/docker";
|
|
13
10
|
import { getLogDir } from "../lib/xdg-log";
|
|
@@ -593,9 +590,7 @@ async function showAwsLogs(
|
|
|
593
590
|
export async function logs(): Promise<void> {
|
|
594
591
|
const opts = parseArgs();
|
|
595
592
|
|
|
596
|
-
const entry = opts.name
|
|
597
|
-
? findAssistantByName(opts.name)
|
|
598
|
-
: loadLatestAssistant();
|
|
593
|
+
const entry = resolveAssistant(opts.name);
|
|
599
594
|
|
|
600
595
|
if (!entry) {
|
|
601
596
|
if (opts.name) {
|