@vellumai/cli 0.6.6 → 0.7.1
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 +8 -2
- package/README.md +49 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +1 -7
- package/src/__tests__/backup.test.ts +475 -0
- package/src/__tests__/config-utils.test.ts +146 -0
- package/src/__tests__/env-drift.test.ts +10 -32
- package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
- package/src/__tests__/multi-local.test.ts +0 -5
- package/src/__tests__/sleep.test.ts +1 -2
- package/src/__tests__/teleport.test.ts +988 -1266
- package/src/commands/backup.ts +117 -71
- package/src/commands/client.ts +10 -9
- package/src/commands/env.ts +93 -0
- package/src/commands/events.ts +2 -0
- package/src/commands/exec.ts +58 -13
- package/src/commands/login.ts +77 -12
- package/src/commands/logs.ts +2 -7
- package/src/commands/ps.ts +144 -25
- package/src/commands/restore.ts +26 -47
- package/src/commands/sleep.ts +5 -2
- package/src/commands/ssh.ts +17 -7
- package/src/commands/teleport.ts +462 -584
- package/src/commands/terminal.ts +9 -221
- package/src/commands/tunnel.ts +2 -7
- package/src/commands/upgrade.ts +108 -7
- package/src/commands/wake.ts +2 -1
- package/src/components/DefaultMainScreen.tsx +328 -154
- package/src/index.ts +5 -7
- package/src/lib/__tests__/docker.test.ts +50 -74
- package/src/lib/__tests__/job-polling.test.ts +278 -0
- package/src/lib/__tests__/local-runtime-client.test.ts +480 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
- package/src/lib/__tests__/runtime-url.test.ts +87 -0
- package/src/lib/__tests__/terminal-session.test.ts +202 -0
- package/src/lib/assistant-client.ts +5 -21
- package/src/lib/assistant-config.ts +46 -24
- package/src/lib/cli-error.ts +1 -0
- package/src/lib/client-identity.ts +67 -0
- package/src/lib/docker.ts +75 -77
- package/src/lib/environments/__tests__/paths.test.ts +2 -0
- package/src/lib/environments/resolve.ts +89 -7
- package/src/lib/environments/seeds.ts +8 -5
- package/src/lib/environments/types.ts +10 -0
- package/src/lib/hatch-local.ts +15 -120
- package/src/lib/health-check.ts +98 -0
- package/src/lib/job-polling.ts +195 -0
- package/src/lib/local-runtime-client.ts +231 -0
- package/src/lib/local.ts +165 -72
- package/src/lib/orphan-detection.ts +2 -35
- package/src/lib/platform-client.ts +190 -194
- package/src/lib/platform-releases.ts +23 -0
- package/src/lib/retire-local.ts +6 -2
- package/src/lib/runtime-url.ts +30 -0
- package/src/lib/sync-cloud-assistants.ts +126 -0
- package/src/lib/terminal-client.ts +6 -1
- package/src/lib/terminal-session.ts +536 -0
- package/src/lib/tui-log.ts +60 -0
- package/src/lib/xdg-log.ts +10 -4
- package/src/shared/provider-env-vars.ts +2 -3
- package/src/__tests__/orphan-detection.test.ts +0 -214
package/src/commands/backup.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
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
|
+
localRuntimePollJobStatus,
|
|
13
|
+
} from "../lib/local-runtime-client.js";
|
|
14
|
+
import {
|
|
15
|
+
platformRequestSignedUrl,
|
|
8
16
|
readPlatformToken,
|
|
9
|
-
platformInitiateExport,
|
|
10
|
-
platformPollExportStatus,
|
|
11
|
-
platformDownloadExport,
|
|
12
17
|
} from "../lib/platform-client.js";
|
|
13
18
|
|
|
14
19
|
export async function backup(): Promise<void> {
|
|
@@ -73,7 +78,7 @@ export async function backup(): Promise<void> {
|
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
if (cloud === "vellum") {
|
|
76
|
-
await backupPlatform(name, outputArg
|
|
81
|
+
await backupPlatform(entry, name, outputArg);
|
|
77
82
|
return;
|
|
78
83
|
}
|
|
79
84
|
|
|
@@ -188,39 +193,66 @@ export async function backup(): Promise<void> {
|
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
// ---------------------------------------------------------------------------
|
|
191
|
-
// Platform (
|
|
196
|
+
// Platform-managed (cloud="vellum") backup over GCS.
|
|
197
|
+
//
|
|
198
|
+
// The runtime exports the bundle straight to a platform-issued signed GCS
|
|
199
|
+
// URL; the CLI then downloads from GCS to local disk. Bytes never flow
|
|
200
|
+
// through Django. Same architectural shape as the platform-source half of
|
|
201
|
+
// `vellum teleport`. Output format and success log lines match mode 1
|
|
202
|
+
// (runtime-direct local backup) so users see one consistent UX.
|
|
203
|
+
//
|
|
204
|
+
// Lifecycle: the GCS bucket has a 1-day TTL on `uploads/<org>/*` objects
|
|
205
|
+
// (see `vellum-assistant-platform/django/app/assistant/migration/views.py`
|
|
206
|
+
// and `migration/services.py`). Backup is single-shot with no import to
|
|
207
|
+
// trigger best-effort cleanup, so the bundle sits in GCS up to 24h before
|
|
208
|
+
// TTL deletion. No explicit cleanup endpoint exists; relying on TTL is
|
|
209
|
+
// intentional.
|
|
192
210
|
// ---------------------------------------------------------------------------
|
|
193
|
-
|
|
194
211
|
async function backupPlatform(
|
|
212
|
+
entry: AssistantEntry,
|
|
195
213
|
name: string,
|
|
196
214
|
outputArg?: string,
|
|
197
|
-
runtimeUrl?: string,
|
|
198
215
|
): Promise<void> {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
216
|
+
const platformToken = readPlatformToken();
|
|
217
|
+
if (!platformToken) {
|
|
218
|
+
console.error(
|
|
219
|
+
"Not logged in. Run 'vellum login' first (required for platform-managed backup).",
|
|
220
|
+
);
|
|
203
221
|
process.exit(1);
|
|
204
222
|
}
|
|
205
|
-
|
|
206
|
-
//
|
|
223
|
+
// Pin upload, download, and runtime requests to the same platform instance
|
|
224
|
+
// the assistant lives on. Using `getPlatformUrl()` instead would target
|
|
225
|
+
// whatever the lockfile / env-var resolves to, which may differ from
|
|
226
|
+
// `entry.runtimeUrl` for staging/dev assistants and end up signing URLs
|
|
227
|
+
// for the wrong GCS bucket. Mirrors the teleport bundlePlatformUrl
|
|
228
|
+
// threading at `cli/src/commands/teleport.ts:1311-1312`.
|
|
229
|
+
const platformUrl = entry.runtimeUrl;
|
|
230
|
+
// Track the working platform token across kickoff/poll/download so a
|
|
231
|
+
// 401-driven refresh during polling stays consistent through the final
|
|
232
|
+
// signed-download request.
|
|
233
|
+
let exportPlatformToken = platformToken;
|
|
234
|
+
|
|
235
|
+
// Step 1 — Request a signed upload URL.
|
|
236
|
+
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
237
|
+
{ operation: "upload" },
|
|
238
|
+
exportPlatformToken,
|
|
239
|
+
platformUrl,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Step 2 — Kick off runtime export-to-GCS through the platform's
|
|
243
|
+
// wildcard runtime proxy. `localRuntimeExportToGcs` builds the
|
|
244
|
+
// `/v1/assistants/<id>/migrations/export-to-gcs` URL for cloud="vellum"
|
|
245
|
+
// and uses platform-token auth (no guardian-token bootstrap).
|
|
207
246
|
let jobId: string;
|
|
208
247
|
try {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
"CLI backup",
|
|
212
|
-
|
|
213
|
-
);
|
|
214
|
-
jobId = result.jobId;
|
|
248
|
+
({ jobId } = await localRuntimeExportToGcs(entry, exportPlatformToken, {
|
|
249
|
+
uploadUrl,
|
|
250
|
+
description: "CLI backup",
|
|
251
|
+
}));
|
|
215
252
|
} 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")) {
|
|
253
|
+
if (err instanceof MigrationInProgressError) {
|
|
222
254
|
console.error(
|
|
223
|
-
|
|
255
|
+
`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
256
|
);
|
|
225
257
|
process.exit(1);
|
|
226
258
|
}
|
|
@@ -229,65 +261,79 @@ async function backupPlatform(
|
|
|
229
261
|
|
|
230
262
|
console.log(`Export started (job ${jobId})...`);
|
|
231
263
|
|
|
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;
|
|
264
|
+
// Step 3 — Poll the job through the wildcard proxy. The dedicated
|
|
265
|
+
// `/v1/migrations/jobs/{id}/` endpoint queries platform-side ImportJob
|
|
266
|
+
// records and would 404 on runtime-created job IDs.
|
|
267
|
+
const terminal = await pollJobUntilDone({
|
|
268
|
+
label: "platform export",
|
|
269
|
+
poll: () => localRuntimePollJobStatus(entry, exportPlatformToken, jobId),
|
|
270
|
+
refreshOn401: async () => {
|
|
271
|
+
const refreshed = readPlatformToken();
|
|
272
|
+
if (!refreshed) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
"Platform auth expired during export and no credential was found on disk. Run 'vellum login' and retry.",
|
|
275
|
+
);
|
|
247
276
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
277
|
+
exportPlatformToken = refreshed;
|
|
278
|
+
},
|
|
279
|
+
});
|
|
252
280
|
|
|
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));
|
|
281
|
+
if (terminal.status === "failed") {
|
|
282
|
+
console.error(`Error: Export failed: ${terminal.error}`);
|
|
283
|
+
process.exit(1);
|
|
265
284
|
}
|
|
266
285
|
|
|
267
|
-
|
|
268
|
-
|
|
286
|
+
// Step 4 — Request a signed download URL for the same bundle and fetch
|
|
287
|
+
// it from GCS directly. No auth on signed URLs.
|
|
288
|
+
// Use `exportPlatformToken` (not the original `platformToken`) so a
|
|
289
|
+
// poll-loop 401 refresh doesn't get clobbered here — otherwise a long
|
|
290
|
+
// export that recovered mid-poll via re-auth would still 401 on the
|
|
291
|
+
// download-URL request and abort an otherwise successful run.
|
|
292
|
+
const { url: bundleUrl } = await platformRequestSignedUrl(
|
|
293
|
+
{ operation: "download", bundleKey },
|
|
294
|
+
exportPlatformToken,
|
|
295
|
+
platformUrl,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
let downloadResponse: Response;
|
|
299
|
+
try {
|
|
300
|
+
downloadResponse = await fetch(bundleUrl);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
303
|
+
console.error(`Error: Failed to fetch bundle from GCS: ${msg}`);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
if (!downloadResponse.ok) {
|
|
307
|
+
const body = await downloadResponse.text().catch(() => "");
|
|
308
|
+
console.error(
|
|
309
|
+
`Error: Failed to fetch bundle from GCS (${downloadResponse.status}): ${body}`,
|
|
310
|
+
);
|
|
269
311
|
process.exit(1);
|
|
270
312
|
}
|
|
271
313
|
|
|
272
|
-
|
|
314
|
+
const arrayBuffer = await downloadResponse.arrayBuffer();
|
|
315
|
+
const data = new Uint8Array(arrayBuffer);
|
|
316
|
+
|
|
317
|
+
// Step 5 — Write to disk using the same path resolution mode 1 uses.
|
|
273
318
|
const isoTimestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
274
319
|
const outputPath =
|
|
275
320
|
outputArg || join(getBackupsDir(), `${name}-${isoTimestamp}.vbundle`);
|
|
276
|
-
|
|
277
321
|
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
322
|
writeFileSync(outputPath, data);
|
|
284
323
|
|
|
285
|
-
// Step
|
|
324
|
+
// Step 6 — Print success. Manifest SHA is included only if the runtime
|
|
325
|
+
// surfaced it via the unified job result; the export-to-gcs runtime
|
|
326
|
+
// route does not set the legacy `X-Vbundle-Manifest-Sha256` response
|
|
327
|
+
// header.
|
|
286
328
|
console.log(`Backup saved to ${outputPath}`);
|
|
287
329
|
console.log(`Size: ${formatSize(data.byteLength)}`);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
330
|
+
const manifestSha =
|
|
331
|
+
terminal.status === "complete" &&
|
|
332
|
+
terminal.result &&
|
|
333
|
+
typeof terminal.result === "object"
|
|
334
|
+
? (terminal.result as Record<string, unknown>).manifest_sha256
|
|
335
|
+
: undefined;
|
|
336
|
+
if (typeof manifestSha === "string") {
|
|
291
337
|
console.log(`Manifest SHA-256: ${manifestSha}`);
|
|
292
338
|
}
|
|
293
339
|
}
|
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,8 @@ 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 { tuiLog } from "../lib/tui-log";
|
|
15
16
|
|
|
16
17
|
const ANSI = {
|
|
17
18
|
reset: "\x1b[0m",
|
|
@@ -76,8 +77,8 @@ function parseArgs(): ParsedArgs {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
if (!entry && hasExplicitUrl) {
|
|
79
|
-
// URL provided but active assistant missing or unset —
|
|
80
|
-
entry =
|
|
80
|
+
// URL provided but active assistant missing or unset — resolve for remaining defaults
|
|
81
|
+
entry = resolveAssistant();
|
|
81
82
|
} else if (!entry) {
|
|
82
83
|
console.error(
|
|
83
84
|
"No active assistant set. Set one with 'vellum use <name>' or specify a name: 'vellum client <name>'.",
|
|
@@ -140,11 +141,6 @@ function maybeSwapToLocalhost(url: string): string {
|
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
const macHost = getMacLocalHostname();
|
|
144
|
-
if (macHost) {
|
|
145
|
-
localNames.push(macHost.toLowerCase());
|
|
146
|
-
}
|
|
147
|
-
|
|
148
144
|
const lanIp = getLocalLanIPv4();
|
|
149
145
|
if (lanIp) {
|
|
150
146
|
localNames.push(lanIp);
|
|
@@ -188,6 +184,9 @@ export async function client(): Promise<void> {
|
|
|
188
184
|
const { runtimeUrl, assistantId, species, bearerToken, project, zone } =
|
|
189
185
|
parseArgs();
|
|
190
186
|
|
|
187
|
+
tuiLog.init();
|
|
188
|
+
tuiLog.info("session start", { runtimeUrl, assistantId, species });
|
|
189
|
+
|
|
191
190
|
const { renderChatApp } = await import("../components/DefaultMainScreen");
|
|
192
191
|
|
|
193
192
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
@@ -197,6 +196,8 @@ export async function client(): Promise<void> {
|
|
|
197
196
|
assistantId,
|
|
198
197
|
species,
|
|
199
198
|
() => {
|
|
199
|
+
tuiLog.info("session end (user disconnect)");
|
|
200
|
+
tuiLog.close();
|
|
200
201
|
app.unmount();
|
|
201
202
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
202
203
|
console.log(`${ANSI.dim}Disconnected.${ANSI.reset}`);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { SEEDS } from "../lib/environments/seeds.js";
|
|
2
|
+
import {
|
|
3
|
+
clearDefaultEnvironment,
|
|
4
|
+
readDefaultEnvironment,
|
|
5
|
+
resolveEnvironmentSource,
|
|
6
|
+
writeDefaultEnvironment,
|
|
7
|
+
} from "../lib/environments/resolve.js";
|
|
8
|
+
|
|
9
|
+
function printUsage(): void {
|
|
10
|
+
console.log("Usage: vellum env <subcommand>");
|
|
11
|
+
console.log("");
|
|
12
|
+
console.log("Manage the default CLI environment.");
|
|
13
|
+
console.log("");
|
|
14
|
+
console.log("Subcommands:");
|
|
15
|
+
console.log(" set <name> Set the default environment");
|
|
16
|
+
console.log(" get Show the current environment and its source");
|
|
17
|
+
console.log(" clear Remove the default, falling back to production");
|
|
18
|
+
console.log("");
|
|
19
|
+
console.log(`Known environments: ${Object.keys(SEEDS).join(", ")}`);
|
|
20
|
+
console.log("");
|
|
21
|
+
console.log("Examples:");
|
|
22
|
+
console.log(" $ vellum env set local # all commands default to local");
|
|
23
|
+
console.log(" $ vellum env get # show resolved environment");
|
|
24
|
+
console.log(" $ vellum env clear # revert to production default");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function envSet(name: string | undefined): void {
|
|
28
|
+
if (!name) {
|
|
29
|
+
console.error(
|
|
30
|
+
`Usage: vellum env set <name>\nKnown environments: ${Object.keys(SEEDS).join(", ")}`,
|
|
31
|
+
);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (!SEEDS[name]) {
|
|
36
|
+
console.error(
|
|
37
|
+
`Unknown environment "${name}". Known environments: ${Object.keys(SEEDS).join(", ")}`,
|
|
38
|
+
);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
writeDefaultEnvironment(name);
|
|
43
|
+
console.log(`Default environment set to "${name}".`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function envGet(): void {
|
|
47
|
+
const { name, source } = resolveEnvironmentSource();
|
|
48
|
+
const sourceLabels: Record<typeof source, string> = {
|
|
49
|
+
flag: "--environment flag",
|
|
50
|
+
env: "VELLUM_ENVIRONMENT env var",
|
|
51
|
+
config: "~/.config/vellum/environment",
|
|
52
|
+
default: "default",
|
|
53
|
+
};
|
|
54
|
+
console.log(`${name} (from ${sourceLabels[source]})`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function envClear(): void {
|
|
58
|
+
const current = readDefaultEnvironment();
|
|
59
|
+
if (!current) {
|
|
60
|
+
console.log("No default environment is set (already using production).");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
clearDefaultEnvironment();
|
|
64
|
+
console.log(
|
|
65
|
+
`Cleared default environment "${current}". Falling back to production.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function env(): Promise<void> {
|
|
70
|
+
const args = process.argv.slice(3);
|
|
71
|
+
const sub = args[0];
|
|
72
|
+
|
|
73
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
74
|
+
printUsage();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
switch (sub) {
|
|
79
|
+
case "set":
|
|
80
|
+
envSet(args[1]);
|
|
81
|
+
break;
|
|
82
|
+
case "get":
|
|
83
|
+
envGet();
|
|
84
|
+
break;
|
|
85
|
+
case "clear":
|
|
86
|
+
envClear();
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
90
|
+
printUsage();
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/commands/events.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { extractFlag } from "../lib/arg-utils.js";
|
|
11
11
|
import { AssistantClient } from "../lib/assistant-client.js";
|
|
12
|
+
import { getClientRegistrationHeaders } from "../lib/client-identity.js";
|
|
12
13
|
|
|
13
14
|
function printUsage(): void {
|
|
14
15
|
console.log(`vellum events - Stream events from a running assistant
|
|
@@ -136,6 +137,7 @@ export async function events(): Promise<void> {
|
|
|
136
137
|
for await (const event of client.stream<AssistantEvent>("/events", {
|
|
137
138
|
signal: controller.signal,
|
|
138
139
|
query,
|
|
140
|
+
headers: getClientRegistrationHeaders(),
|
|
139
141
|
})) {
|
|
140
142
|
if (jsonOutput) {
|
|
141
143
|
console.log(JSON.stringify(event));
|
package/src/commands/exec.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
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";
|
|
7
|
+
import { getPlatformUrl, readPlatformToken } from "../lib/platform-client";
|
|
11
8
|
import { sshAppleContainer } from "../lib/ssh-apple-container";
|
|
9
|
+
import {
|
|
10
|
+
interactiveSession,
|
|
11
|
+
nonInteractiveExec,
|
|
12
|
+
shellEscapeArgs,
|
|
13
|
+
} from "../lib/terminal-session";
|
|
12
14
|
|
|
13
15
|
const SERVICE_ALIASES: Record<string, ServiceName> = {
|
|
14
16
|
assistant: "assistant",
|
|
@@ -74,11 +76,15 @@ export async function exec(): Promise<void> {
|
|
|
74
76
|
);
|
|
75
77
|
console.log("");
|
|
76
78
|
console.log("Options:");
|
|
79
|
+
console.log(" --service <svc> Target service (default: assistant)");
|
|
77
80
|
console.log(
|
|
78
|
-
"
|
|
81
|
+
" -it Interactive mode with TTY (like docker exec -it)",
|
|
79
82
|
);
|
|
80
83
|
console.log(
|
|
81
|
-
"
|
|
84
|
+
" --timeout <secs> Timeout in seconds (default: 30, 0 = no timeout)",
|
|
85
|
+
);
|
|
86
|
+
console.log(
|
|
87
|
+
" --verbose Show debug output (SSE events, sentinel parsing)",
|
|
82
88
|
);
|
|
83
89
|
console.log("");
|
|
84
90
|
console.log("Services:");
|
|
@@ -90,9 +96,7 @@ export async function exec(): Promise<void> {
|
|
|
90
96
|
console.log(" vellum exec -- ls -la /workspace");
|
|
91
97
|
console.log(" vellum exec -- cat /workspace/NOW.md");
|
|
92
98
|
console.log(" vellum exec -it -- /bin/bash");
|
|
93
|
-
console.log(
|
|
94
|
-
" vellum exec --service gateway -- cat /tmp/gateway.log",
|
|
95
|
-
);
|
|
99
|
+
console.log(" vellum exec --service gateway -- cat /tmp/gateway.log");
|
|
96
100
|
process.exit(0);
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -114,12 +118,23 @@ export async function exec(): Promise<void> {
|
|
|
114
118
|
let nameArg: string | undefined;
|
|
115
119
|
let serviceRaw = "assistant";
|
|
116
120
|
let interactive = false;
|
|
121
|
+
let verbose = false;
|
|
122
|
+
let timeoutMs = 30_000;
|
|
117
123
|
|
|
118
124
|
for (let i = 0; i < preArgs.length; i++) {
|
|
119
125
|
if (preArgs[i] === "--service" && preArgs[i + 1]) {
|
|
120
126
|
serviceRaw = preArgs[++i];
|
|
121
127
|
} else if (preArgs[i] === "-it" || preArgs[i] === "-ti") {
|
|
122
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;
|
|
136
|
+
} else if (preArgs[i] === "--verbose") {
|
|
137
|
+
verbose = true;
|
|
123
138
|
} else if (!preArgs[i].startsWith("-")) {
|
|
124
139
|
nameArg = preArgs[i];
|
|
125
140
|
}
|
|
@@ -127,9 +142,7 @@ export async function exec(): Promise<void> {
|
|
|
127
142
|
|
|
128
143
|
const service = normalizeService(serviceRaw);
|
|
129
144
|
|
|
130
|
-
const entry = nameArg
|
|
131
|
-
? findAssistantByName(nameArg)
|
|
132
|
-
: loadLatestAssistant();
|
|
145
|
+
const entry = resolveAssistant(nameArg);
|
|
133
146
|
|
|
134
147
|
if (!entry) {
|
|
135
148
|
if (nameArg) {
|
|
@@ -179,6 +192,38 @@ export async function exec(): Promise<void> {
|
|
|
179
192
|
return;
|
|
180
193
|
}
|
|
181
194
|
|
|
195
|
+
if (cloud === "vellum") {
|
|
196
|
+
const token = readPlatformToken();
|
|
197
|
+
if (!token) {
|
|
198
|
+
console.error(
|
|
199
|
+
"Not logged in. Run `vellum login` first to authenticate with the platform.",
|
|
200
|
+
);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const assistant = {
|
|
205
|
+
assistantId: entry.assistantId,
|
|
206
|
+
token,
|
|
207
|
+
platformUrl: getPlatformUrl(),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const serviceParam = service === "assistant" ? undefined : service;
|
|
211
|
+
|
|
212
|
+
if (interactive) {
|
|
213
|
+
// Interactive mode: shell-escape argv and delegate to full terminal
|
|
214
|
+
await interactiveSession(assistant, shellEscapeArgs(command), serviceParam);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Non-interactive: sentinel-based output capture with exit code
|
|
219
|
+
await nonInteractiveExec(assistant, command, {
|
|
220
|
+
verbose,
|
|
221
|
+
timeoutMs,
|
|
222
|
+
service: serviceParam,
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
182
227
|
console.error(
|
|
183
228
|
`Error: 'vellum exec' is not supported for ${cloud} instances.`,
|
|
184
229
|
);
|