@vellumai/cli 0.8.5 → 0.8.7-dev.202606052118.34cd356
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 +6 -0
- package/bun.lock +8 -0
- package/knip.json +6 -1
- package/node_modules/@vellumai/environments/bun.lock +24 -0
- package/node_modules/@vellumai/environments/package.json +18 -0
- package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +95 -0
- package/node_modules/@vellumai/environments/src/index.ts +11 -0
- package/{src/lib/environments → node_modules/@vellumai/environments/src}/seeds.ts +5 -9
- package/node_modules/@vellumai/environments/tsconfig.json +20 -0
- package/node_modules/@vellumai/local-mode/bun.lock +29 -0
- package/node_modules/@vellumai/local-mode/package.json +22 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +116 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +79 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +108 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +104 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +66 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +66 -0
- package/node_modules/@vellumai/local-mode/src/environment.ts +62 -0
- package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +109 -0
- package/node_modules/@vellumai/local-mode/src/guardian-token.ts +122 -0
- package/node_modules/@vellumai/local-mode/src/hatch.ts +92 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +48 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +173 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +114 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +235 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +133 -0
- package/node_modules/@vellumai/local-mode/src/retire.ts +58 -0
- package/node_modules/@vellumai/local-mode/src/util.ts +102 -0
- package/node_modules/@vellumai/local-mode/src/wake.ts +78 -0
- package/node_modules/@vellumai/local-mode/tsconfig.json +16 -0
- package/package.json +12 -1
- package/src/__tests__/assistant-client-refresh.test.ts +182 -0
- package/src/__tests__/backup.test.ts +38 -0
- package/src/__tests__/clean.test.ts +179 -0
- package/src/__tests__/client-token.test.ts +87 -0
- package/src/__tests__/client-tui-refresh.test.ts +170 -0
- package/src/__tests__/cloudflare-tunnel.test.ts +137 -0
- package/src/__tests__/connect-import.test.ts +317 -0
- package/src/__tests__/devices.test.ts +272 -0
- package/src/__tests__/env-drift.test.ts +32 -44
- package/src/__tests__/flags.test.ts +248 -0
- package/src/__tests__/guardian-token.test.ts +126 -2
- package/src/__tests__/multi-local.test.ts +1 -1
- package/src/__tests__/orphan-detection.test.ts +8 -6
- package/src/__tests__/pair.test.ts +271 -0
- package/src/__tests__/paired-lifecycle.test.ts +116 -0
- package/src/__tests__/recover.test.ts +307 -0
- package/src/__tests__/segments-to-plain-text.test.ts +37 -0
- package/src/__tests__/tui-midsession-refresh.test.ts +166 -0
- package/src/__tests__/unpair.test.ts +163 -0
- package/src/__tests__/wake.test.ts +215 -0
- package/src/commands/backup.ts +2 -0
- package/src/commands/client.ts +569 -39
- package/src/commands/connect/import.ts +217 -0
- package/src/commands/connect.ts +31 -0
- package/src/commands/devices.ts +247 -0
- package/src/commands/env.ts +1 -1
- package/src/commands/flags.ts +269 -0
- package/src/commands/gateway/token.ts +73 -0
- package/src/commands/gateway.ts +29 -0
- package/src/commands/logs.ts +6 -18
- package/src/commands/pair.ts +222 -0
- package/src/commands/ps.ts +57 -41
- package/src/commands/recover.ts +47 -9
- package/src/commands/restore.ts +8 -1
- package/src/commands/retire.ts +23 -70
- package/src/commands/rollback.ts +2 -14
- package/src/commands/sleep.ts +7 -0
- package/src/commands/ssh.ts +5 -24
- package/src/commands/teleport.ts +34 -26
- package/src/commands/tunnel.ts +46 -2
- package/src/commands/unpair.ts +118 -0
- package/src/commands/upgrade.ts +8 -16
- package/src/commands/wake.ts +75 -45
- package/src/components/DefaultMainScreen.tsx +100 -14
- package/src/index.ts +22 -0
- package/src/lib/__tests__/lifecycle-reporter.test.ts +59 -0
- package/src/lib/__tests__/step-runner.test.ts +49 -1
- package/src/lib/assistant-client.ts +58 -37
- package/src/lib/assistant-config.ts +28 -3
- package/src/lib/cloudflare-tunnel.ts +276 -0
- package/src/lib/config-utils.ts +24 -3
- package/src/lib/confirm-action.ts +57 -0
- package/src/lib/docker.ts +82 -8
- package/src/lib/environments/__tests__/paths.test.ts +2 -1
- package/src/lib/environments/__tests__/seeds.test.ts +2 -1
- package/src/lib/environments/paths.ts +1 -1
- package/src/lib/environments/resolve.ts +11 -35
- package/src/lib/guardian-token.ts +132 -9
- package/src/lib/hatch-local.ts +75 -33
- package/src/lib/http-client.ts +1 -3
- package/src/lib/lifecycle-reporter.ts +31 -0
- package/src/lib/local.ts +193 -298
- package/src/lib/orphan-detection.ts +9 -5
- package/src/lib/pgrep.ts +5 -1
- package/src/lib/platform-client.ts +97 -49
- package/src/lib/process.ts +109 -39
- package/src/lib/retire-local.ts +28 -14
- package/src/lib/segments-to-plain-text.ts +35 -0
- package/src/lib/step-runner.ts +67 -7
- package/src/lib/sync-cloud-assistants.ts +17 -0
- /package/{src/lib/environments → node_modules/@vellumai/environments/src}/types.ts +0 -0
package/src/commands/ps.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "path";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
extractHostFromUrl,
|
|
4
5
|
findAssistantByName,
|
|
5
6
|
formatAssistantLookupError,
|
|
6
7
|
formatAssistantReference,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
getDaemonPidPath,
|
|
10
11
|
loadAllAssistants,
|
|
11
12
|
lookupAssistantByIdentifier,
|
|
13
|
+
resolveCloud,
|
|
12
14
|
type AssistantEntry,
|
|
13
15
|
} from "../lib/assistant-config";
|
|
14
16
|
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
@@ -26,7 +28,7 @@ import { existsSync } from "fs";
|
|
|
26
28
|
import {
|
|
27
29
|
classifyProcess,
|
|
28
30
|
detectOrphanedProcesses,
|
|
29
|
-
|
|
31
|
+
isPidAlive,
|
|
30
32
|
parseRemotePs,
|
|
31
33
|
readPidFile,
|
|
32
34
|
} from "../lib/orphan-detection";
|
|
@@ -149,35 +151,25 @@ const REMOTE_PS_CMD = [
|
|
|
149
151
|
"| grep -v grep",
|
|
150
152
|
].join(" ");
|
|
151
153
|
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const parsed = new URL(url);
|
|
155
|
-
return parsed.hostname;
|
|
156
|
-
} catch {
|
|
157
|
-
return url.replace(/^https?:\/\//, "").split(":")[0];
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function resolveCloud(entry: AssistantEntry): string {
|
|
162
|
-
if (entry.cloud) return entry.cloud;
|
|
163
|
-
if (entry.project) return "gcp";
|
|
164
|
-
if (entry.sshUser) return "custom";
|
|
165
|
-
return "local";
|
|
166
|
-
}
|
|
154
|
+
const REMOTE_SSH_TIMEOUT_MS = 30_000;
|
|
167
155
|
|
|
168
156
|
async function getRemoteProcessesGcp(entry: AssistantEntry): Promise<string> {
|
|
169
|
-
return execOutput(
|
|
170
|
-
"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
157
|
+
return execOutput(
|
|
158
|
+
"gcloud",
|
|
159
|
+
[
|
|
160
|
+
"compute",
|
|
161
|
+
"ssh",
|
|
162
|
+
`${entry.sshUser ?? entry.assistantId}@${entry.assistantId}`,
|
|
163
|
+
`--zone=${entry.zone}`,
|
|
164
|
+
`--project=${entry.project}`,
|
|
165
|
+
`--command=${REMOTE_PS_CMD}`,
|
|
166
|
+
"--ssh-flag=-o StrictHostKeyChecking=no",
|
|
167
|
+
"--ssh-flag=-o UserKnownHostsFile=/dev/null",
|
|
168
|
+
"--ssh-flag=-o ConnectTimeout=10",
|
|
169
|
+
"--ssh-flag=-o LogLevel=ERROR",
|
|
170
|
+
],
|
|
171
|
+
{ timeoutMs: REMOTE_SSH_TIMEOUT_MS },
|
|
172
|
+
);
|
|
181
173
|
}
|
|
182
174
|
|
|
183
175
|
async function getRemoteProcessesCustom(
|
|
@@ -185,7 +177,9 @@ async function getRemoteProcessesCustom(
|
|
|
185
177
|
): Promise<string> {
|
|
186
178
|
const host = extractHostFromUrl(entry.runtimeUrl);
|
|
187
179
|
const sshUser = entry.sshUser ?? "root";
|
|
188
|
-
return execOutput("ssh", [...SSH_OPTS, `${sshUser}@${host}`, REMOTE_PS_CMD]
|
|
180
|
+
return execOutput("ssh", [...SSH_OPTS, `${sshUser}@${host}`, REMOTE_PS_CMD], {
|
|
181
|
+
timeoutMs: REMOTE_SSH_TIMEOUT_MS,
|
|
182
|
+
});
|
|
189
183
|
}
|
|
190
184
|
|
|
191
185
|
interface ProcessSpec {
|
|
@@ -203,9 +197,13 @@ interface DetectedProcess {
|
|
|
203
197
|
watch: boolean;
|
|
204
198
|
}
|
|
205
199
|
|
|
200
|
+
const LOCAL_CMD_TIMEOUT_MS = 5_000;
|
|
201
|
+
|
|
206
202
|
async function isWatchMode(pid: string): Promise<boolean> {
|
|
207
203
|
try {
|
|
208
|
-
const args = await execOutput("ps", ["-p", pid, "-o", "args="]
|
|
204
|
+
const args = await execOutput("ps", ["-p", pid, "-o", "args="], {
|
|
205
|
+
timeoutMs: LOCAL_CMD_TIMEOUT_MS,
|
|
206
|
+
});
|
|
209
207
|
return args.includes("--watch");
|
|
210
208
|
} catch {
|
|
211
209
|
return false;
|
|
@@ -242,7 +240,7 @@ async function detectProcess(spec: ProcessSpec): Promise<DetectedProcess> {
|
|
|
242
240
|
|
|
243
241
|
// Tier 3: PID file fallback
|
|
244
242
|
const filePid = readPidFile(spec.pidFile);
|
|
245
|
-
if (filePid &&
|
|
243
|
+
if (filePid && isPidAlive(filePid)) {
|
|
246
244
|
const watch = await isWatchMode(filePid);
|
|
247
245
|
return {
|
|
248
246
|
name: spec.name,
|
|
@@ -320,12 +318,11 @@ async function getDockerContainerState(
|
|
|
320
318
|
containerName: string,
|
|
321
319
|
): Promise<string | null> {
|
|
322
320
|
try {
|
|
323
|
-
const output = await execOutput(
|
|
324
|
-
"
|
|
325
|
-
"--format",
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
]);
|
|
321
|
+
const output = await execOutput(
|
|
322
|
+
"docker",
|
|
323
|
+
["inspect", "--format", "{{.State.Status}}", containerName],
|
|
324
|
+
{ timeoutMs: LOCAL_CMD_TIMEOUT_MS },
|
|
325
|
+
);
|
|
329
326
|
return output.trim() || "unknown";
|
|
330
327
|
} catch {
|
|
331
328
|
return null;
|
|
@@ -447,6 +444,22 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
447
444
|
return;
|
|
448
445
|
}
|
|
449
446
|
|
|
447
|
+
if (cloud === "paired") {
|
|
448
|
+
// A remote assistant paired from another machine: no local process to
|
|
449
|
+
// list — probe the remote gateway's health over the bearer token instead.
|
|
450
|
+
const token = loadGuardianToken(entry.assistantId)?.accessToken;
|
|
451
|
+
const health = await checkHealth(entry.runtimeUrl, token);
|
|
452
|
+
const rows: TableRow[] = [
|
|
453
|
+
{
|
|
454
|
+
name: "gateway",
|
|
455
|
+
status: withStatusEmoji(health.status),
|
|
456
|
+
info: entry.runtimeUrl + (health.detail ? ` | ${health.detail}` : ""),
|
|
457
|
+
},
|
|
458
|
+
];
|
|
459
|
+
printTable(rows);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
450
463
|
let output: string;
|
|
451
464
|
try {
|
|
452
465
|
if (cloud === "gcp") {
|
|
@@ -458,9 +471,12 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
458
471
|
process.exit(1);
|
|
459
472
|
}
|
|
460
473
|
} catch (error) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
474
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
475
|
+
if (msg.includes("timed out")) {
|
|
476
|
+
console.warn(`Warning: remote process listing timed out — ${msg}`);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
console.error(`Failed to list processes: ${msg}`);
|
|
464
480
|
process.exit(1);
|
|
465
481
|
}
|
|
466
482
|
|
|
@@ -496,7 +512,7 @@ async function getAssistantListHealth(
|
|
|
496
512
|
// TODO(ATL-306): Remove readPidFile/getDaemonPidPath in favor of
|
|
497
513
|
// fetching daemon PIDs via the health API (Gateway Security Migration).
|
|
498
514
|
const pid = readPidFile(getDaemonPidPath(resources));
|
|
499
|
-
const alive = pid !== null &&
|
|
515
|
+
const alive = pid !== null && isPidAlive(pid);
|
|
500
516
|
if (!alive) {
|
|
501
517
|
return { status: "sleeping", detail: null };
|
|
502
518
|
}
|
package/src/commands/recover.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
} from "fs";
|
|
2
8
|
import { homedir } from "os";
|
|
3
|
-
import { join } from "path";
|
|
9
|
+
import { basename, dirname, join } from "path";
|
|
4
10
|
|
|
5
11
|
import { saveAssistantEntry } from "../lib/assistant-config";
|
|
6
12
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
@@ -21,8 +27,22 @@ export async function recover(): Promise<void> {
|
|
|
21
27
|
"Restore a previously retired local assistant from its archive.",
|
|
22
28
|
);
|
|
23
29
|
console.log("");
|
|
30
|
+
console.log(
|
|
31
|
+
"Extracts the archived workspace data back to its original location,",
|
|
32
|
+
);
|
|
33
|
+
console.log(
|
|
34
|
+
"restores the lockfile entry, and starts the assistant and gateway.",
|
|
35
|
+
);
|
|
36
|
+
console.log(
|
|
37
|
+
"Archives are stored in $XDG_DATA_HOME/vellum/retired/ (default: ~/.local/share/vellum/retired/).",
|
|
38
|
+
);
|
|
39
|
+
console.log("");
|
|
24
40
|
console.log("Arguments:");
|
|
25
41
|
console.log(" <name> Name of the retired assistant to recover");
|
|
42
|
+
console.log("");
|
|
43
|
+
console.log("Examples:");
|
|
44
|
+
console.log(" $ vellum recover my-assistant");
|
|
45
|
+
console.log(" $ vellum recover aria-7f3a");
|
|
26
46
|
process.exit(0);
|
|
27
47
|
}
|
|
28
48
|
|
|
@@ -61,11 +81,27 @@ export async function recover(): Promise<void> {
|
|
|
61
81
|
process.exit(1);
|
|
62
82
|
}
|
|
63
83
|
|
|
64
|
-
// 4.
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
84
|
+
// 4. Determine the original target directory, then extract and rename.
|
|
85
|
+
//
|
|
86
|
+
// retireLocal archives either the full instanceDir (named instances) or just
|
|
87
|
+
// the .vellum/ subdirectory (default instance whose instanceDir === homedir()).
|
|
88
|
+
// The directory is staged under `<archive>.staging` inside the retired dir
|
|
89
|
+
// before being packed with `tar -C <retiredDir> <stagingBasename>`, so the
|
|
90
|
+
// top-level entry inside the tarball is always `<name>.tar.gz.staging`.
|
|
91
|
+
//
|
|
92
|
+
// Correct restoration: extract to retiredDir, then rename the staging entry
|
|
93
|
+
// back to the original target path. Using homedir() as the -C target was
|
|
94
|
+
// wrong for any instance stored outside the home directory.
|
|
95
|
+
const isNamedInstance = entry.resources.instanceDir !== homedir();
|
|
96
|
+
const targetDir = isNamedInstance
|
|
97
|
+
? entry.resources.instanceDir
|
|
98
|
+
: join(entry.resources.instanceDir, ".vellum");
|
|
99
|
+
const retiredDir = dirname(archivePath);
|
|
100
|
+
const extractedPath = join(retiredDir, basename(archivePath) + ".staging");
|
|
101
|
+
|
|
102
|
+
await exec("tar", ["xzf", archivePath, "-C", retiredDir]);
|
|
103
|
+
mkdirSync(dirname(targetDir), { recursive: true });
|
|
104
|
+
renameSync(extractedPath, targetDir);
|
|
69
105
|
|
|
70
106
|
// 5. Restore lockfile entry
|
|
71
107
|
saveAssistantEntry(entry);
|
|
@@ -74,14 +110,16 @@ export async function recover(): Promise<void> {
|
|
|
74
110
|
unlinkSync(archivePath);
|
|
75
111
|
unlinkSync(metadataPath);
|
|
76
112
|
|
|
77
|
-
// 7. Persist signing key so
|
|
113
|
+
// 7. Persist signing key and bootstrap secret so they survive daemon/gateway restarts
|
|
78
114
|
const signingKey = generateLocalSigningKey();
|
|
115
|
+
const bootstrapSecret = generateLocalSigningKey();
|
|
79
116
|
entry.resources = { ...entry.resources, signingKey };
|
|
117
|
+
entry.guardianBootstrapSecret = bootstrapSecret;
|
|
80
118
|
saveAssistantEntry(entry);
|
|
81
119
|
|
|
82
120
|
// 8. Start daemon + gateway
|
|
83
121
|
await startLocalDaemon(false, entry.resources, { signingKey });
|
|
84
|
-
await startGateway(false, entry.resources, { signingKey });
|
|
122
|
+
await startGateway(false, entry.resources, { signingKey, bootstrapSecret });
|
|
85
123
|
|
|
86
124
|
console.log(`✅ Recovered assistant '${name}'.`);
|
|
87
125
|
}
|
package/src/commands/restore.ts
CHANGED
|
@@ -97,6 +97,7 @@ async function getAccessToken(
|
|
|
97
97
|
runtimeUrl: string,
|
|
98
98
|
assistantId: string,
|
|
99
99
|
displayName: string,
|
|
100
|
+
bootstrapSecret?: string,
|
|
100
101
|
): Promise<string> {
|
|
101
102
|
const tokenData = loadGuardianToken(assistantId);
|
|
102
103
|
|
|
@@ -105,7 +106,11 @@ async function getAccessToken(
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
try {
|
|
108
|
-
const freshToken = await leaseGuardianToken(
|
|
109
|
+
const freshToken = await leaseGuardianToken(
|
|
110
|
+
runtimeUrl,
|
|
111
|
+
assistantId,
|
|
112
|
+
bootstrapSecret,
|
|
113
|
+
);
|
|
109
114
|
return freshToken.accessToken;
|
|
110
115
|
} catch (err) {
|
|
111
116
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -574,6 +579,7 @@ export async function restore(): Promise<void> {
|
|
|
574
579
|
entry.runtimeUrl,
|
|
575
580
|
entry.assistantId,
|
|
576
581
|
name,
|
|
582
|
+
entry.guardianBootstrapSecret,
|
|
577
583
|
);
|
|
578
584
|
|
|
579
585
|
if (dryRun) {
|
|
@@ -679,6 +685,7 @@ export async function restore(): Promise<void> {
|
|
|
679
685
|
entry.runtimeUrl,
|
|
680
686
|
entry.assistantId,
|
|
681
687
|
name,
|
|
688
|
+
entry.guardianBootstrapSecret,
|
|
682
689
|
);
|
|
683
690
|
}
|
|
684
691
|
|
package/src/commands/retire.ts
CHANGED
|
@@ -2,15 +2,21 @@ import { existsSync, unlinkSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
extractHostFromUrl,
|
|
5
6
|
formatAssistantLookupError,
|
|
6
7
|
formatAssistantReference,
|
|
7
8
|
getAssistantDisplayName,
|
|
8
9
|
loadAllAssistants,
|
|
9
10
|
lookupAssistantByIdentifier,
|
|
10
11
|
removeAssistantEntry,
|
|
12
|
+
resolveCloud,
|
|
13
|
+
type AssistantEntry,
|
|
11
14
|
} from "../lib/assistant-config.js";
|
|
12
|
-
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
13
15
|
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
16
|
+
import {
|
|
17
|
+
canPromptForConfirmation,
|
|
18
|
+
confirmAction,
|
|
19
|
+
} from "../lib/confirm-action.js";
|
|
14
20
|
import { getConfigDir } from "../lib/environments/paths.js";
|
|
15
21
|
import { getCurrentEnvironment } from "../lib/environments/resolve.js";
|
|
16
22
|
import {
|
|
@@ -31,28 +37,6 @@ import {
|
|
|
31
37
|
writeToLogFile,
|
|
32
38
|
} from "../lib/xdg-log.js";
|
|
33
39
|
|
|
34
|
-
function resolveCloud(entry: AssistantEntry): string {
|
|
35
|
-
if (entry.cloud) {
|
|
36
|
-
return entry.cloud;
|
|
37
|
-
}
|
|
38
|
-
if (entry.project) {
|
|
39
|
-
return "gcp";
|
|
40
|
-
}
|
|
41
|
-
if (entry.sshUser) {
|
|
42
|
-
return "custom";
|
|
43
|
-
}
|
|
44
|
-
return "local";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function extractHostFromUrl(url: string): string {
|
|
48
|
-
try {
|
|
49
|
-
const parsed = new URL(url);
|
|
50
|
-
return parsed.hostname;
|
|
51
|
-
} catch {
|
|
52
|
-
return url.replace(/^https?:\/\//, "").split(":")[0];
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
40
|
export { retireLocal };
|
|
57
41
|
|
|
58
42
|
interface RetireArgs {
|
|
@@ -172,51 +156,6 @@ function printRetireTarget(entry: AssistantEntry, cloud: string): void {
|
|
|
172
156
|
console.log("");
|
|
173
157
|
}
|
|
174
158
|
|
|
175
|
-
function canPromptForRetireConfirmation(): boolean {
|
|
176
|
-
return (
|
|
177
|
-
process.stdin.isTTY === true &&
|
|
178
|
-
process.stdout.isTTY === true &&
|
|
179
|
-
typeof process.stdin.setRawMode === "function"
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function confirmRetireInteractive(): Promise<boolean> {
|
|
184
|
-
const stdin = process.stdin;
|
|
185
|
-
const stdout = process.stdout;
|
|
186
|
-
const wasRaw = stdin.isRaw === true;
|
|
187
|
-
const wasPaused = stdin.isPaused();
|
|
188
|
-
|
|
189
|
-
stdout.write("Press Enter to retire, or Esc/q to cancel: ");
|
|
190
|
-
stdin.setRawMode(true);
|
|
191
|
-
stdin.resume();
|
|
192
|
-
|
|
193
|
-
return await new Promise<boolean>((resolve) => {
|
|
194
|
-
const cleanup = () => {
|
|
195
|
-
stdin.off("data", onData);
|
|
196
|
-
stdin.setRawMode(wasRaw);
|
|
197
|
-
if (wasPaused) {
|
|
198
|
-
stdin.pause();
|
|
199
|
-
}
|
|
200
|
-
stdout.write("\n");
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const onData = (chunk: Buffer) => {
|
|
204
|
-
const byte = chunk[0];
|
|
205
|
-
if (byte === 13 || byte === 10) {
|
|
206
|
-
cleanup();
|
|
207
|
-
resolve(true);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
if (byte === 27 || byte === 3 || byte === 113 || byte === 81) {
|
|
211
|
-
cleanup();
|
|
212
|
-
resolve(false);
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
stdin.on("data", onData);
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
159
|
/** Patch console methods to also append output to the given log file descriptor. */
|
|
221
160
|
function teeConsoleToLogFile(fd: number | "ignore"): void {
|
|
222
161
|
if (fd === "ignore") return;
|
|
@@ -310,10 +249,22 @@ async function retireInner(): Promise<void> {
|
|
|
310
249
|
const assistantId = entry.assistantId;
|
|
311
250
|
const source = parsed.source;
|
|
312
251
|
const cloud = resolveCloud(entry);
|
|
252
|
+
|
|
253
|
+
if (cloud === "paired") {
|
|
254
|
+
// A remote assistant paired from another machine. Retiring tears the
|
|
255
|
+
// assistant down — that can only happen on its host machine, never from a
|
|
256
|
+
// paired machine, which holds nothing but a pairing record. (Removing that
|
|
257
|
+
// local record is `vellum unpair`'s job, not retire's.)
|
|
258
|
+
console.error(
|
|
259
|
+
`Error: '${assistantId}' is a remote assistant paired from another machine — it can't be retired from here. Retiring tears down the assistant, which can only be done on its host machine. To remove the local pairing record on this machine, run \`vellum unpair ${assistantId}\`.`,
|
|
260
|
+
);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
313
264
|
printRetireTarget(entry, cloud);
|
|
314
265
|
|
|
315
266
|
if (!parsed.yes) {
|
|
316
|
-
if (!
|
|
267
|
+
if (!canPromptForConfirmation()) {
|
|
317
268
|
console.error(
|
|
318
269
|
"Error: Refusing to retire without confirmation in a non-interactive terminal.",
|
|
319
270
|
);
|
|
@@ -321,7 +272,9 @@ async function retireInner(): Promise<void> {
|
|
|
321
272
|
process.exit(1);
|
|
322
273
|
}
|
|
323
274
|
|
|
324
|
-
const confirmed = await
|
|
275
|
+
const confirmed = await confirmAction(
|
|
276
|
+
"Press Enter to retire, or Esc/q to cancel: ",
|
|
277
|
+
);
|
|
325
278
|
if (!confirmed) {
|
|
326
279
|
console.log("Retire cancelled.");
|
|
327
280
|
process.exit(1);
|
package/src/commands/rollback.ts
CHANGED
|
@@ -4,9 +4,10 @@ import {
|
|
|
4
4
|
findAssistantByName,
|
|
5
5
|
getActiveAssistant,
|
|
6
6
|
loadAllAssistants,
|
|
7
|
+
resolveCloud,
|
|
7
8
|
saveAssistantEntry,
|
|
9
|
+
type AssistantEntry,
|
|
8
10
|
} from "../lib/assistant-config";
|
|
9
|
-
import type { AssistantEntry } from "../lib/assistant-config";
|
|
10
11
|
import {
|
|
11
12
|
captureImageRefs,
|
|
12
13
|
GATEWAY_INTERNAL_PORT,
|
|
@@ -90,19 +91,6 @@ function parseArgs(): { name: string | null; version: string | null } {
|
|
|
90
91
|
return { name, version };
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
function resolveCloud(entry: AssistantEntry): string {
|
|
94
|
-
if (entry.cloud) {
|
|
95
|
-
return entry.cloud;
|
|
96
|
-
}
|
|
97
|
-
if (entry.project) {
|
|
98
|
-
return "gcp";
|
|
99
|
-
}
|
|
100
|
-
if (entry.sshUser) {
|
|
101
|
-
return "custom";
|
|
102
|
-
}
|
|
103
|
-
return "local";
|
|
104
|
-
}
|
|
105
|
-
|
|
106
94
|
/**
|
|
107
95
|
* Resolve which assistant to target for the rollback command. Priority:
|
|
108
96
|
* 1. Explicit name argument
|
package/src/commands/sleep.ts
CHANGED
|
@@ -82,6 +82,13 @@ export async function sleep(): Promise<void> {
|
|
|
82
82
|
process.exit(1);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
if (entry.cloud === "paired") {
|
|
86
|
+
console.error(
|
|
87
|
+
`Error: '${entry.assistantId}' is a remote assistant paired from another machine — its lifecycle is managed on its host machine, not here. Use \`vellum client ${entry.assistantId}\` to chat with it.`,
|
|
88
|
+
);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
if (entry.cloud && entry.cloud !== "local") {
|
|
86
93
|
console.error(
|
|
87
94
|
`Error: 'vellum sleep' only works with local and docker assistants. '${entry.assistantId}' is a ${entry.cloud} instance.`,
|
package/src/commands/ssh.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
extractHostFromUrl,
|
|
5
|
+
resolveAssistant,
|
|
6
|
+
resolveCloud,
|
|
7
|
+
} from "../lib/assistant-config";
|
|
5
8
|
import { dockerResourceNames } from "../lib/docker";
|
|
6
9
|
import { getPlatformUrl, readPlatformToken } from "../lib/platform-client";
|
|
7
10
|
import { sshAppleContainer } from "../lib/ssh-apple-container";
|
|
@@ -18,28 +21,6 @@ const SSH_OPTS = [
|
|
|
18
21
|
"LogLevel=ERROR",
|
|
19
22
|
];
|
|
20
23
|
|
|
21
|
-
function resolveCloud(entry: AssistantEntry): string {
|
|
22
|
-
if (entry.cloud) {
|
|
23
|
-
return entry.cloud;
|
|
24
|
-
}
|
|
25
|
-
if (entry.project) {
|
|
26
|
-
return "gcp";
|
|
27
|
-
}
|
|
28
|
-
if (entry.sshUser) {
|
|
29
|
-
return "custom";
|
|
30
|
-
}
|
|
31
|
-
return "local";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function extractHostFromUrl(url: string): string {
|
|
35
|
-
try {
|
|
36
|
-
const parsed = new URL(url);
|
|
37
|
-
return parsed.hostname;
|
|
38
|
-
} catch {
|
|
39
|
-
return url.replace(/^https?:\/\//, "").split(":")[0];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
24
|
export async function ssh(): Promise<void> {
|
|
44
25
|
const args = process.argv.slice(3);
|
|
45
26
|
if (args.includes("--help") || args.includes("-h")) {
|
package/src/commands/teleport.ts
CHANGED
|
@@ -3,10 +3,11 @@ import {
|
|
|
3
3
|
loadAllAssistants,
|
|
4
4
|
getDaemonPidPath,
|
|
5
5
|
removeAssistantEntry,
|
|
6
|
+
resolveCloud,
|
|
6
7
|
saveAssistantEntry,
|
|
7
8
|
setActiveAssistant,
|
|
9
|
+
type AssistantEntry,
|
|
8
10
|
} from "../lib/assistant-config.js";
|
|
9
|
-
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
10
11
|
import {
|
|
11
12
|
loadGuardianToken,
|
|
12
13
|
leaseGuardianToken,
|
|
@@ -214,12 +215,6 @@ export function parseArgs(argv: string[]): {
|
|
|
214
215
|
return { from, to, targetEnv, targetName, keepSource, dryRun, help };
|
|
215
216
|
}
|
|
216
217
|
|
|
217
|
-
function resolveCloud(entry: AssistantEntry): string {
|
|
218
|
-
return (
|
|
219
|
-
entry.cloud || (entry.project ? "gcp" : entry.sshUser ? "custom" : "local")
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
218
|
// ---------------------------------------------------------------------------
|
|
224
219
|
// Auth helper — same pattern as restore.ts
|
|
225
220
|
// ---------------------------------------------------------------------------
|
|
@@ -228,7 +223,7 @@ async function getAccessToken(
|
|
|
228
223
|
runtimeUrl: string,
|
|
229
224
|
assistantId: string,
|
|
230
225
|
displayName: string,
|
|
231
|
-
options?: { forceRefresh?: boolean },
|
|
226
|
+
options?: { forceRefresh?: boolean; bootstrapSecret?: string },
|
|
232
227
|
): Promise<string> {
|
|
233
228
|
// When forceRefresh is set (e.g. after a runtime 401 on the cached token)
|
|
234
229
|
// we skip the cache and lease a brand-new token from the gateway, so a
|
|
@@ -242,7 +237,11 @@ async function getAccessToken(
|
|
|
242
237
|
}
|
|
243
238
|
|
|
244
239
|
try {
|
|
245
|
-
const freshToken = await leaseGuardianToken(
|
|
240
|
+
const freshToken = await leaseGuardianToken(
|
|
241
|
+
runtimeUrl,
|
|
242
|
+
assistantId,
|
|
243
|
+
options?.bootstrapSecret,
|
|
244
|
+
);
|
|
246
245
|
return freshToken.accessToken;
|
|
247
246
|
} catch (err) {
|
|
248
247
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -281,11 +280,15 @@ function isRuntime401(err: unknown): boolean {
|
|
|
281
280
|
* — propagates to the caller.
|
|
282
281
|
*/
|
|
283
282
|
async function callRuntimeWithAuthRetry<T>(
|
|
284
|
-
|
|
285
|
-
assistantId: string,
|
|
283
|
+
entry: AssistantEntry,
|
|
286
284
|
fn: (token: string) => Promise<T>,
|
|
287
285
|
): Promise<T> {
|
|
288
|
-
const firstToken = await getAccessToken(
|
|
286
|
+
const firstToken = await getAccessToken(
|
|
287
|
+
entry.runtimeUrl,
|
|
288
|
+
entry.assistantId,
|
|
289
|
+
entry.assistantId,
|
|
290
|
+
{ bootstrapSecret: entry.guardianBootstrapSecret },
|
|
291
|
+
);
|
|
289
292
|
try {
|
|
290
293
|
return await fn(firstToken);
|
|
291
294
|
} catch (err) {
|
|
@@ -293,10 +296,13 @@ async function callRuntimeWithAuthRetry<T>(
|
|
|
293
296
|
throw err;
|
|
294
297
|
}
|
|
295
298
|
const refreshedToken = await getAccessToken(
|
|
296
|
-
runtimeUrl,
|
|
297
|
-
assistantId,
|
|
298
|
-
assistantId,
|
|
299
|
-
{
|
|
299
|
+
entry.runtimeUrl,
|
|
300
|
+
entry.assistantId,
|
|
301
|
+
entry.assistantId,
|
|
302
|
+
{
|
|
303
|
+
forceRefresh: true,
|
|
304
|
+
bootstrapSecret: entry.guardianBootstrapSecret,
|
|
305
|
+
},
|
|
300
306
|
);
|
|
301
307
|
return await fn(refreshedToken);
|
|
302
308
|
}
|
|
@@ -386,8 +392,7 @@ async function exportFromAssistant(
|
|
|
386
392
|
let sourceRuntimeVersion: string;
|
|
387
393
|
try {
|
|
388
394
|
const identity = await callRuntimeWithAuthRetry(
|
|
389
|
-
entry
|
|
390
|
-
entry.assistantId,
|
|
395
|
+
entry,
|
|
391
396
|
async (token) => localRuntimeIdentity(entry, token),
|
|
392
397
|
);
|
|
393
398
|
sourceRuntimeVersion = identity.version;
|
|
@@ -423,8 +428,7 @@ async function exportFromAssistant(
|
|
|
423
428
|
let accessToken: string;
|
|
424
429
|
try {
|
|
425
430
|
const result = await callRuntimeWithAuthRetry(
|
|
426
|
-
entry
|
|
427
|
-
entry.assistantId,
|
|
431
|
+
entry,
|
|
428
432
|
async (token) => {
|
|
429
433
|
const r = await localRuntimeExportToGcs(entry, token, {
|
|
430
434
|
uploadUrl,
|
|
@@ -462,7 +466,10 @@ async function exportFromAssistant(
|
|
|
462
466
|
entry.runtimeUrl,
|
|
463
467
|
entry.assistantId,
|
|
464
468
|
entry.assistantId,
|
|
465
|
-
{
|
|
469
|
+
{
|
|
470
|
+
forceRefresh: true,
|
|
471
|
+
bootstrapSecret: entry.guardianBootstrapSecret,
|
|
472
|
+
},
|
|
466
473
|
);
|
|
467
474
|
},
|
|
468
475
|
});
|
|
@@ -728,8 +735,7 @@ async function importToAssistant(
|
|
|
728
735
|
let targetRuntimeVersion: string;
|
|
729
736
|
try {
|
|
730
737
|
const identity = await callRuntimeWithAuthRetry(
|
|
731
|
-
entry
|
|
732
|
-
entry.assistantId,
|
|
738
|
+
entry,
|
|
733
739
|
(token) => localRuntimeIdentity(entry, token),
|
|
734
740
|
);
|
|
735
741
|
targetRuntimeVersion = identity.version;
|
|
@@ -774,8 +780,7 @@ async function importToAssistant(
|
|
|
774
780
|
let accessToken: string;
|
|
775
781
|
try {
|
|
776
782
|
const result = await callRuntimeWithAuthRetry(
|
|
777
|
-
entry
|
|
778
|
-
entry.assistantId,
|
|
783
|
+
entry,
|
|
779
784
|
async (token) => {
|
|
780
785
|
const r = await localRuntimeImportFromGcs(entry, token, {
|
|
781
786
|
bundleUrl,
|
|
@@ -806,7 +811,10 @@ async function importToAssistant(
|
|
|
806
811
|
entry.runtimeUrl,
|
|
807
812
|
entry.assistantId,
|
|
808
813
|
entry.assistantId,
|
|
809
|
-
{
|
|
814
|
+
{
|
|
815
|
+
forceRefresh: true,
|
|
816
|
+
bootstrapSecret: entry.guardianBootstrapSecret,
|
|
817
|
+
},
|
|
810
818
|
);
|
|
811
819
|
},
|
|
812
820
|
});
|