@vellumai/cli 0.7.1 → 0.7.3
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/bun.lock +0 -15
- package/package.json +1 -6
- package/src/__tests__/backup.test.ts +121 -5
- package/src/__tests__/teleport.test.ts +515 -10
- package/src/commands/backup.ts +35 -2
- package/src/commands/client.ts +90 -7
- package/src/commands/exec.ts +13 -4
- package/src/commands/hatch.ts +1 -1
- package/src/commands/login.ts +11 -0
- package/src/commands/restore.ts +7 -1
- package/src/commands/rollback.ts +1 -1
- package/src/commands/setup.ts +38 -73
- package/src/commands/teleport.ts +122 -12
- package/src/commands/upgrade.ts +8 -2
- package/src/commands/wake.ts +5 -16
- package/src/components/DefaultMainScreen.tsx +42 -130
- package/src/index.ts +1 -7
- package/src/lib/__tests__/docker.test.ts +53 -35
- package/src/lib/__tests__/local-runtime-client.test.ts +186 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +235 -0
- package/src/lib/__tests__/runtime-url.test.ts +39 -1
- package/src/lib/assistant-client.ts +13 -5
- package/src/lib/assistant-config.ts +0 -25
- package/src/lib/backup-ops.ts +43 -17
- package/src/lib/client-identity.ts +9 -5
- package/src/lib/docker.ts +6 -267
- package/src/lib/environments/paths.ts +20 -0
- package/src/lib/guardian-token.ts +56 -6
- package/src/lib/hatch-local.ts +3 -26
- package/src/lib/local-runtime-client.ts +82 -1
- package/src/lib/local.ts +9 -7
- package/src/lib/ngrok.ts +36 -26
- package/src/lib/platform-client.ts +100 -1
- package/src/lib/retire-local.ts +2 -2
- package/src/lib/runtime-url.ts +22 -0
- package/src/lib/statefulset.ts +375 -0
- package/src/lib/upgrade-lifecycle.ts +97 -1
- package/src/commands/pair.ts +0 -212
package/src/commands/pair.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { createHash } from "crypto";
|
|
2
|
-
import { readFileSync } from "fs";
|
|
3
|
-
import jsQR from "jsqr";
|
|
4
|
-
import { hostname, userInfo } from "os";
|
|
5
|
-
import { PNG } from "pngjs";
|
|
6
|
-
|
|
7
|
-
import { saveAssistantEntry } from "../lib/assistant-config";
|
|
8
|
-
import type { AssistantEntry } from "../lib/assistant-config";
|
|
9
|
-
import type { Species } from "../lib/constants";
|
|
10
|
-
import { saveGuardianToken } from "../lib/guardian-token";
|
|
11
|
-
import type { GuardianTokenData } from "../lib/guardian-token";
|
|
12
|
-
import { generateInstanceName } from "../lib/random-name";
|
|
13
|
-
|
|
14
|
-
interface QRPairingPayload {
|
|
15
|
-
type: string;
|
|
16
|
-
v: number;
|
|
17
|
-
id?: string;
|
|
18
|
-
g: string;
|
|
19
|
-
pairingRequestId: string;
|
|
20
|
-
pairingSecret: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface PairingResponse {
|
|
24
|
-
status: "approved" | "pending";
|
|
25
|
-
bearerToken?: string;
|
|
26
|
-
gatewayUrl?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function decodeQRCodeFromPng(pngPath: string): string {
|
|
30
|
-
const fileData = readFileSync(pngPath);
|
|
31
|
-
const png = PNG.sync.read(fileData);
|
|
32
|
-
const code = jsQR(new Uint8ClampedArray(png.data), png.width, png.height);
|
|
33
|
-
if (!code) {
|
|
34
|
-
throw new Error("Could not decode QR code from the provided PNG image.");
|
|
35
|
-
}
|
|
36
|
-
return code.data;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function safeUserInfoUsername(): string {
|
|
40
|
-
try {
|
|
41
|
-
return userInfo().username;
|
|
42
|
-
} catch {
|
|
43
|
-
return "";
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getDeviceId(): string {
|
|
48
|
-
const raw = hostname() + safeUserInfoUsername();
|
|
49
|
-
return createHash("sha256").update(raw).digest("hex");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const PAIRING_POLL_INTERVAL_MS = 2000;
|
|
53
|
-
const PAIRING_POLL_TIMEOUT_MS = 120_000;
|
|
54
|
-
|
|
55
|
-
async function pollForApproval(
|
|
56
|
-
gatewayUrl: string,
|
|
57
|
-
pairingRequestId: string,
|
|
58
|
-
pairingSecret: string,
|
|
59
|
-
): Promise<PairingResponse> {
|
|
60
|
-
const startTime = Date.now();
|
|
61
|
-
|
|
62
|
-
while (Date.now() - startTime < PAIRING_POLL_TIMEOUT_MS) {
|
|
63
|
-
const statusUrl = `${gatewayUrl}/pairing/status?id=${encodeURIComponent(pairingRequestId)}&secret=${encodeURIComponent(pairingSecret)}`;
|
|
64
|
-
const statusRes = await fetch(statusUrl);
|
|
65
|
-
|
|
66
|
-
if (!statusRes.ok) {
|
|
67
|
-
const body = await statusRes.text().catch(() => "");
|
|
68
|
-
throw new Error(
|
|
69
|
-
`Failed to check pairing status: HTTP ${statusRes.status}: ${body || statusRes.statusText}`,
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const statusBody = (await statusRes.json()) as PairingResponse;
|
|
74
|
-
|
|
75
|
-
if (statusBody.status === "approved") {
|
|
76
|
-
return statusBody;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
await new Promise((resolve) =>
|
|
80
|
-
setTimeout(resolve, PAIRING_POLL_INTERVAL_MS),
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
throw new Error("Pairing timed out waiting for approval.");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function pair(): Promise<void> {
|
|
88
|
-
const args = process.argv.slice(3);
|
|
89
|
-
|
|
90
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
91
|
-
console.log("Usage: vellum pair <path-to-qrcode.png>");
|
|
92
|
-
console.log("");
|
|
93
|
-
console.log(
|
|
94
|
-
"Pair with a remote assistant by scanning the QR code PNG generated during setup.",
|
|
95
|
-
);
|
|
96
|
-
process.exit(0);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const qrCodePath = args[0] || process.env.VELLUM_CUSTOM_QR_CODE_PATH;
|
|
100
|
-
|
|
101
|
-
if (!qrCodePath) {
|
|
102
|
-
console.error("Usage: vellum pair <path-to-qrcode.png>");
|
|
103
|
-
console.error("");
|
|
104
|
-
console.error(
|
|
105
|
-
"Pair with a remote assistant by scanning the QR code PNG generated during setup.",
|
|
106
|
-
);
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const species: Species = "vellum";
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
console.log("Reading QR code from provided image...");
|
|
114
|
-
const qrData = decodeQRCodeFromPng(qrCodePath);
|
|
115
|
-
|
|
116
|
-
let payload: QRPairingPayload;
|
|
117
|
-
try {
|
|
118
|
-
payload = JSON.parse(qrData) as QRPairingPayload;
|
|
119
|
-
} catch {
|
|
120
|
-
throw new Error("QR code does not contain valid pairing data.");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
payload.type !== "vellum-daemon" ||
|
|
125
|
-
!payload.g ||
|
|
126
|
-
!payload.pairingRequestId ||
|
|
127
|
-
!payload.pairingSecret
|
|
128
|
-
) {
|
|
129
|
-
throw new Error("QR code does not contain valid Vellum pairing data.");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const instanceName = generateInstanceName(species);
|
|
133
|
-
const runtimeUrl = payload.g;
|
|
134
|
-
const deviceId = getDeviceId();
|
|
135
|
-
const deviceName = hostname();
|
|
136
|
-
|
|
137
|
-
console.log(`Pairing with remote assistant at ${runtimeUrl}...`);
|
|
138
|
-
|
|
139
|
-
const requestUrl = `${runtimeUrl}/pairing/request`;
|
|
140
|
-
const requestRes = await fetch(requestUrl, {
|
|
141
|
-
method: "POST",
|
|
142
|
-
headers: { "Content-Type": "application/json" },
|
|
143
|
-
body: JSON.stringify({
|
|
144
|
-
pairingRequestId: payload.pairingRequestId,
|
|
145
|
-
pairingSecret: payload.pairingSecret,
|
|
146
|
-
deviceId,
|
|
147
|
-
deviceName,
|
|
148
|
-
}),
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
if (!requestRes.ok) {
|
|
152
|
-
const body = await requestRes.text().catch(() => "");
|
|
153
|
-
throw new Error(
|
|
154
|
-
`Failed to initiate pairing: HTTP ${requestRes.status}: ${body || requestRes.statusText}`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const requestBody = (await requestRes.json()) as PairingResponse;
|
|
159
|
-
|
|
160
|
-
let bearerToken: string | undefined;
|
|
161
|
-
|
|
162
|
-
if (requestBody.status === "approved") {
|
|
163
|
-
bearerToken = requestBody.bearerToken;
|
|
164
|
-
} else if (requestBody.status === "pending") {
|
|
165
|
-
console.log("Waiting for pairing approval...");
|
|
166
|
-
const approvedResponse = await pollForApproval(
|
|
167
|
-
runtimeUrl,
|
|
168
|
-
payload.pairingRequestId,
|
|
169
|
-
payload.pairingSecret,
|
|
170
|
-
);
|
|
171
|
-
bearerToken = approvedResponse.bearerToken;
|
|
172
|
-
} else {
|
|
173
|
-
throw new Error(
|
|
174
|
-
`Unexpected pairing response status: ${requestBody.status}`,
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const customEntry: AssistantEntry = {
|
|
179
|
-
assistantId: instanceName,
|
|
180
|
-
runtimeUrl,
|
|
181
|
-
cloud: "custom",
|
|
182
|
-
species,
|
|
183
|
-
hatchedAt: new Date().toISOString(),
|
|
184
|
-
};
|
|
185
|
-
saveAssistantEntry(customEntry);
|
|
186
|
-
|
|
187
|
-
if (bearerToken) {
|
|
188
|
-
const tokenData: GuardianTokenData = {
|
|
189
|
-
guardianPrincipalId: "",
|
|
190
|
-
accessToken: bearerToken,
|
|
191
|
-
accessTokenExpiresAt: "",
|
|
192
|
-
refreshToken: "",
|
|
193
|
-
refreshTokenExpiresAt: "",
|
|
194
|
-
refreshAfter: "",
|
|
195
|
-
isNew: true,
|
|
196
|
-
deviceId: getDeviceId(),
|
|
197
|
-
leasedAt: new Date().toISOString(),
|
|
198
|
-
};
|
|
199
|
-
saveGuardianToken(instanceName, tokenData);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
console.log("");
|
|
203
|
-
console.log("Successfully paired with remote assistant!");
|
|
204
|
-
console.log("Instance details:");
|
|
205
|
-
console.log(` Name: ${instanceName}`);
|
|
206
|
-
console.log(` Runtime URL: ${runtimeUrl}`);
|
|
207
|
-
console.log("");
|
|
208
|
-
} catch (error) {
|
|
209
|
-
console.error("Error:", error instanceof Error ? error.message : error);
|
|
210
|
-
process.exit(1);
|
|
211
|
-
}
|
|
212
|
-
}
|