@vellumai/cli 0.3.11 → 0.3.13
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/README.md +8 -8
- package/bun.lock +6 -0
- package/package.json +5 -2
- package/src/__tests__/assistant-config.test.ts +139 -0
- package/src/__tests__/constants.test.ts +48 -0
- package/src/__tests__/health-check.test.ts +64 -0
- package/src/__tests__/random-name.test.ts +19 -0
- package/src/__tests__/retire-archive.test.ts +30 -0
- package/src/__tests__/status-emoji.test.ts +44 -0
- package/src/commands/autonomy.ts +321 -0
- package/src/commands/client.ts +15 -8
- package/src/commands/contacts.ts +265 -0
- package/src/commands/hatch.ts +93 -37
- package/src/commands/login.ts +68 -0
- package/src/commands/ps.ts +7 -3
- package/src/commands/recover.ts +1 -1
- package/src/commands/retire.ts +2 -2
- package/src/commands/skills.ts +355 -0
- package/src/commands/ssh.ts +5 -2
- package/src/components/DefaultMainScreen.tsx +67 -3
- package/src/index.ts +23 -0
- package/src/lib/assistant-config.ts +1 -0
- package/src/lib/gcp.ts +9 -13
- package/src/lib/local.ts +23 -28
- package/src/lib/platform-client.ts +74 -0
- package/src/types/sh.d.ts +4 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join, dirname } from "node:path";
|
|
10
|
+
import { gunzipSync } from "node:zlib";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Path helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function getRootDir(): string {
|
|
18
|
+
return join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getSkillsDir(): string {
|
|
22
|
+
return join(getRootDir(), "workspace", "skills");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getSkillsIndexPath(): string {
|
|
26
|
+
return join(getSkillsDir(), "SKILLS.md");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Platform API client
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
function getConfigPlatformUrl(): string | undefined {
|
|
34
|
+
try {
|
|
35
|
+
const configPath = join(getRootDir(), "workspace", "config.json");
|
|
36
|
+
if (!existsSync(configPath)) return undefined;
|
|
37
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8")) as Record<
|
|
38
|
+
string,
|
|
39
|
+
unknown
|
|
40
|
+
>;
|
|
41
|
+
const platform = raw.platform as Record<string, unknown> | undefined;
|
|
42
|
+
const baseUrl = platform?.baseUrl;
|
|
43
|
+
if (typeof baseUrl === "string" && baseUrl.trim()) return baseUrl.trim();
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getPlatformUrl(): string {
|
|
51
|
+
return (
|
|
52
|
+
process.env.VELLUM_ASSISTANT_PLATFORM_URL ??
|
|
53
|
+
getConfigPlatformUrl() ??
|
|
54
|
+
"https://platform.vellum.ai"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getPlatformToken(): string | null {
|
|
59
|
+
try {
|
|
60
|
+
return readFileSync(join(getRootDir(), "platform-token"), "utf-8").trim();
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildHeaders(): Record<string, string> {
|
|
67
|
+
const headers: Record<string, string> = {};
|
|
68
|
+
const token = getPlatformToken();
|
|
69
|
+
if (token) {
|
|
70
|
+
headers["X-Session-Token"] = token;
|
|
71
|
+
}
|
|
72
|
+
return headers;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Types
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
interface CatalogSkill {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
description: string;
|
|
83
|
+
emoji?: string;
|
|
84
|
+
includes?: string[];
|
|
85
|
+
version?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface CatalogManifest {
|
|
89
|
+
version: number;
|
|
90
|
+
skills: CatalogSkill[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Catalog operations
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
async function fetchCatalog(): Promise<CatalogSkill[]> {
|
|
98
|
+
const url = `${getPlatformUrl()}/v1/skills/`;
|
|
99
|
+
const response = await fetch(url, {
|
|
100
|
+
headers: buildHeaders(),
|
|
101
|
+
signal: AbortSignal.timeout(10000),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw new Error(`Platform API error ${response.status}: ${response.statusText}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const manifest = (await response.json()) as CatalogManifest;
|
|
109
|
+
if (!Array.isArray(manifest.skills)) {
|
|
110
|
+
throw new Error("Platform catalog has invalid skills array");
|
|
111
|
+
}
|
|
112
|
+
return manifest.skills;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extract SKILL.md content from a tar archive (uncompressed).
|
|
117
|
+
*/
|
|
118
|
+
function extractSkillMdFromTar(tarBuffer: Buffer): string | null {
|
|
119
|
+
let offset = 0;
|
|
120
|
+
while (offset + 512 <= tarBuffer.length) {
|
|
121
|
+
const header = tarBuffer.subarray(offset, offset + 512);
|
|
122
|
+
|
|
123
|
+
// End-of-archive (two consecutive zero blocks)
|
|
124
|
+
if (header.every((b) => b === 0)) break;
|
|
125
|
+
|
|
126
|
+
// Filename (bytes 0-99, null-terminated)
|
|
127
|
+
const nameEnd = header.indexOf(0, 0);
|
|
128
|
+
const name = header
|
|
129
|
+
.subarray(0, Math.min(nameEnd >= 0 ? nameEnd : 100, 100))
|
|
130
|
+
.toString("utf-8");
|
|
131
|
+
|
|
132
|
+
// File size (bytes 124-135, octal)
|
|
133
|
+
const sizeStr = header.subarray(124, 136).toString("utf-8").trim();
|
|
134
|
+
const size = parseInt(sizeStr, 8) || 0;
|
|
135
|
+
|
|
136
|
+
offset += 512; // past header
|
|
137
|
+
|
|
138
|
+
if (name.endsWith("SKILL.md") || name === "SKILL.md") {
|
|
139
|
+
return tarBuffer.subarray(offset, offset + size).toString("utf-8");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Skip to next header (data padded to 512 bytes)
|
|
143
|
+
offset += Math.ceil(size / 512) * 512;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function fetchSkillContent(skillId: string): Promise<string> {
|
|
149
|
+
const url = `${getPlatformUrl()}/v1/skills/${encodeURIComponent(skillId)}/`;
|
|
150
|
+
const response = await fetch(url, {
|
|
151
|
+
headers: buildHeaders(),
|
|
152
|
+
signal: AbortSignal.timeout(15000),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Failed to fetch skill "${skillId}": HTTP ${response.status}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const gzipBuffer = Buffer.from(await response.arrayBuffer());
|
|
162
|
+
const tarBuffer = gunzipSync(gzipBuffer);
|
|
163
|
+
const skillMd = extractSkillMdFromTar(tarBuffer);
|
|
164
|
+
|
|
165
|
+
if (!skillMd) {
|
|
166
|
+
throw new Error(`SKILL.md not found in archive for "${skillId}"`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return skillMd;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Managed skill installation
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
function atomicWriteFile(filePath: string, content: string): void {
|
|
177
|
+
const dir = dirname(filePath);
|
|
178
|
+
mkdirSync(dir, { recursive: true });
|
|
179
|
+
const tmpPath = join(dir, `.tmp-${randomUUID()}`);
|
|
180
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
181
|
+
renameSync(tmpPath, filePath);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function upsertSkillsIndex(id: string): void {
|
|
185
|
+
const indexPath = getSkillsIndexPath();
|
|
186
|
+
let lines: string[] = [];
|
|
187
|
+
if (existsSync(indexPath)) {
|
|
188
|
+
lines = readFileSync(indexPath, "utf-8").split("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
192
|
+
const pattern = new RegExp(`^[-*]\\s+(?:\`)?${escaped}(?:\`)?\\s*$`);
|
|
193
|
+
if (lines.some((line) => pattern.test(line))) return;
|
|
194
|
+
|
|
195
|
+
const nonEmpty = lines.filter((l) => l.trim());
|
|
196
|
+
nonEmpty.push(`- ${id}`);
|
|
197
|
+
const content = nonEmpty.join("\n");
|
|
198
|
+
atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function installSkillLocally(
|
|
202
|
+
skillId: string,
|
|
203
|
+
skillMdContent: string,
|
|
204
|
+
catalogEntry: CatalogSkill,
|
|
205
|
+
overwrite: boolean,
|
|
206
|
+
): void {
|
|
207
|
+
const skillDir = join(getSkillsDir(), skillId);
|
|
208
|
+
const skillFilePath = join(skillDir, "SKILL.md");
|
|
209
|
+
|
|
210
|
+
if (existsSync(skillFilePath) && !overwrite) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Skill "${skillId}" is already installed. Use --overwrite to replace it.`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
mkdirSync(skillDir, { recursive: true });
|
|
217
|
+
atomicWriteFile(skillFilePath, skillMdContent);
|
|
218
|
+
|
|
219
|
+
// Write version metadata
|
|
220
|
+
if (catalogEntry.version) {
|
|
221
|
+
const meta = {
|
|
222
|
+
version: catalogEntry.version,
|
|
223
|
+
installedAt: new Date().toISOString(),
|
|
224
|
+
};
|
|
225
|
+
atomicWriteFile(
|
|
226
|
+
join(skillDir, "version.json"),
|
|
227
|
+
JSON.stringify(meta, null, 2) + "\n",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
upsertSkillsIndex(skillId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Helpers
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
function hasFlag(args: string[], flag: string): boolean {
|
|
239
|
+
return args.includes(flag);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Usage
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
function printUsage(): void {
|
|
247
|
+
console.log("Usage: vellum skills <subcommand> [options]");
|
|
248
|
+
console.log("");
|
|
249
|
+
console.log("Subcommands:");
|
|
250
|
+
console.log(" list List available catalog skills");
|
|
251
|
+
console.log(
|
|
252
|
+
" install <skill-id> [--overwrite] Install a skill from the catalog",
|
|
253
|
+
);
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log("Options:");
|
|
256
|
+
console.log(" --json Machine-readable JSON output");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Command entry point
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
export async function skills(): Promise<void> {
|
|
264
|
+
const args = process.argv.slice(3);
|
|
265
|
+
const subcommand = args[0];
|
|
266
|
+
const json = hasFlag(args, "--json");
|
|
267
|
+
|
|
268
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
269
|
+
printUsage();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
switch (subcommand) {
|
|
274
|
+
case "list": {
|
|
275
|
+
try {
|
|
276
|
+
const catalog = await fetchCatalog();
|
|
277
|
+
|
|
278
|
+
if (json) {
|
|
279
|
+
console.log(JSON.stringify({ ok: true, skills: catalog }));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (catalog.length === 0) {
|
|
284
|
+
console.log("No skills available in the catalog.");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log(`Available skills (${catalog.length}):\n`);
|
|
289
|
+
for (const s of catalog) {
|
|
290
|
+
const emoji = s.emoji ? `${s.emoji} ` : "";
|
|
291
|
+
const deps = s.includes?.length
|
|
292
|
+
? ` (requires: ${s.includes.join(", ")})`
|
|
293
|
+
: "";
|
|
294
|
+
console.log(` ${emoji}${s.id}`);
|
|
295
|
+
console.log(` ${s.name} — ${s.description}${deps}`);
|
|
296
|
+
}
|
|
297
|
+
} catch (err) {
|
|
298
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
299
|
+
if (json) {
|
|
300
|
+
console.log(JSON.stringify({ ok: false, error: msg }));
|
|
301
|
+
} else {
|
|
302
|
+
console.error(`Error: ${msg}`);
|
|
303
|
+
}
|
|
304
|
+
process.exitCode = 1;
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
case "install": {
|
|
310
|
+
const skillId = args.find((a) => !a.startsWith("--") && a !== "install");
|
|
311
|
+
if (!skillId) {
|
|
312
|
+
console.error("Usage: vellum skills install <skill-id>");
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const overwrite = hasFlag(args, "--overwrite");
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
// Verify skill exists in catalog
|
|
320
|
+
const catalog = await fetchCatalog();
|
|
321
|
+
const entry = catalog.find((s) => s.id === skillId);
|
|
322
|
+
if (!entry) {
|
|
323
|
+
throw new Error(`Skill "${skillId}" not found in the Vellum catalog`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Fetch SKILL.md from platform
|
|
327
|
+
const content = await fetchSkillContent(skillId);
|
|
328
|
+
|
|
329
|
+
// Install locally
|
|
330
|
+
installSkillLocally(skillId, content, entry, overwrite);
|
|
331
|
+
|
|
332
|
+
if (json) {
|
|
333
|
+
console.log(JSON.stringify({ ok: true, skillId }));
|
|
334
|
+
} else {
|
|
335
|
+
console.log(`Installed skill "${skillId}".`);
|
|
336
|
+
}
|
|
337
|
+
} catch (err) {
|
|
338
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
339
|
+
if (json) {
|
|
340
|
+
console.log(JSON.stringify({ ok: false, error: msg }));
|
|
341
|
+
} else {
|
|
342
|
+
console.error(`Error: ${msg}`);
|
|
343
|
+
}
|
|
344
|
+
process.exitCode = 1;
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
default: {
|
|
350
|
+
console.error(`Unknown skills subcommand: ${subcommand}`);
|
|
351
|
+
printUsage();
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
package/src/commands/ssh.ts
CHANGED
|
@@ -40,7 +40,7 @@ export async function ssh(): Promise<void> {
|
|
|
40
40
|
if (name) {
|
|
41
41
|
console.error(`No assistant instance found with name '${name}'.`);
|
|
42
42
|
} else {
|
|
43
|
-
console.error("No assistant instance found. Run `vellum
|
|
43
|
+
console.error("No assistant instance found. Run `vellum hatch` first.");
|
|
44
44
|
}
|
|
45
45
|
process.exit(1);
|
|
46
46
|
}
|
|
@@ -50,7 +50,7 @@ export async function ssh(): Promise<void> {
|
|
|
50
50
|
if (cloud === "local") {
|
|
51
51
|
console.error(
|
|
52
52
|
"Cannot SSH into a local assistant. Local assistants run directly on this machine.\n" +
|
|
53
|
-
`Use 'vellum
|
|
53
|
+
`Use 'vellum ps ${entry.assistantId}' to check its processes instead.`,
|
|
54
54
|
);
|
|
55
55
|
process.exit(1);
|
|
56
56
|
}
|
|
@@ -81,6 +81,9 @@ export async function ssh(): Promise<void> {
|
|
|
81
81
|
["compute", "ssh", sshTarget, `--project=${project}`, `--zone=${zone}`],
|
|
82
82
|
{ stdio: "inherit" },
|
|
83
83
|
);
|
|
84
|
+
} else if (cloud === "vellum") {
|
|
85
|
+
console.error("SSH to Vellum-managed instances is not yet supported.");
|
|
86
|
+
process.exit(1);
|
|
84
87
|
} else if (cloud === "custom") {
|
|
85
88
|
const host = extractHostFromUrl(entry.runtimeUrl);
|
|
86
89
|
const sshUser = entry.sshUser ?? "root";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import { randomUUID } from "crypto";
|
|
2
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
3
3
|
import { basename } from "path";
|
|
4
|
+
import qrcode from "qrcode-terminal";
|
|
4
5
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from "react";
|
|
5
6
|
import { Box, render as inkRender, Text, useInput, useStdout } from "ink";
|
|
6
7
|
|
|
@@ -23,7 +24,7 @@ export const ANSI = {
|
|
|
23
24
|
gray: "\x1b[90m",
|
|
24
25
|
} as const;
|
|
25
26
|
|
|
26
|
-
export const SLASH_COMMANDS = ["/clear", "/doctor", "/exit", "/help", "/q", "/quit", "/retire"];
|
|
27
|
+
export const SLASH_COMMANDS = ["/clear", "/doctor", "/exit", "/help", "/pair", "/q", "/quit", "/retire"];
|
|
27
28
|
|
|
28
29
|
const POLL_INTERVAL_MS = 3000;
|
|
29
30
|
const SEND_TIMEOUT_MS = 5000;
|
|
@@ -157,7 +158,7 @@ async function sendMessage(
|
|
|
157
158
|
"/messages",
|
|
158
159
|
{
|
|
159
160
|
method: "POST",
|
|
160
|
-
body: JSON.stringify({ conversationKey: assistantId, content }),
|
|
161
|
+
body: JSON.stringify({ conversationKey: assistantId, content, sourceChannel: "vellum", interface: "cli" }),
|
|
161
162
|
signal,
|
|
162
163
|
},
|
|
163
164
|
bearerToken,
|
|
@@ -529,6 +530,10 @@ function HelpDisplay(): ReactElement {
|
|
|
529
530
|
{" /clear "}
|
|
530
531
|
<Text dimColor>Clear the screen</Text>
|
|
531
532
|
</Text>
|
|
533
|
+
<Text>
|
|
534
|
+
{" /pair "}
|
|
535
|
+
<Text dimColor>Generate a QR code for mobile device pairing</Text>
|
|
536
|
+
</Text>
|
|
532
537
|
<Text>
|
|
533
538
|
{" /help, ? "}
|
|
534
539
|
<Text dimColor>Show this help</Text>
|
|
@@ -1280,6 +1285,65 @@ function ChatApp({
|
|
|
1280
1285
|
return;
|
|
1281
1286
|
}
|
|
1282
1287
|
|
|
1288
|
+
if (trimmed === "/pair") {
|
|
1289
|
+
h.showSpinner("Generating pairing credentials...");
|
|
1290
|
+
|
|
1291
|
+
const isConnected = await ensureConnected();
|
|
1292
|
+
if (!isConnected) {
|
|
1293
|
+
h.hideSpinner();
|
|
1294
|
+
h.showError("Cannot pair — not connected to the assistant runtime.");
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
try {
|
|
1299
|
+
const pairingRequestId = randomUUID();
|
|
1300
|
+
const pairingSecret = randomBytes(32).toString("hex");
|
|
1301
|
+
const gatewayUrl = runtimeUrl;
|
|
1302
|
+
|
|
1303
|
+
// Call /v1/pairing/register directly (not under /v1/assistants/:id/)
|
|
1304
|
+
const registerUrl = `${runtimeUrl}/v1/pairing/register`;
|
|
1305
|
+
const registerRes = await fetch(registerUrl, {
|
|
1306
|
+
method: "POST",
|
|
1307
|
+
headers: {
|
|
1308
|
+
"Content-Type": "application/json",
|
|
1309
|
+
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
|
|
1310
|
+
},
|
|
1311
|
+
body: JSON.stringify({ pairingRequestId, pairingSecret, gatewayUrl }),
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
if (!registerRes.ok) {
|
|
1315
|
+
const body = await registerRes.text().catch(() => "");
|
|
1316
|
+
throw new Error(`HTTP ${registerRes.status}: ${body || registerRes.statusText}`);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const payload = JSON.stringify({
|
|
1320
|
+
type: "vellum-daemon",
|
|
1321
|
+
v: 4,
|
|
1322
|
+
g: gatewayUrl,
|
|
1323
|
+
pairingRequestId,
|
|
1324
|
+
pairingSecret,
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
const qrString = await new Promise<string>((resolve) => {
|
|
1328
|
+
qrcode.generate(payload, { small: true }, (code: string) => {
|
|
1329
|
+
resolve(code);
|
|
1330
|
+
});
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
h.hideSpinner();
|
|
1334
|
+
h.addStatus(
|
|
1335
|
+
`Pairing Ready\n\n` +
|
|
1336
|
+
`Scan this QR code with the Vellum iOS app:\n\n` +
|
|
1337
|
+
`${qrString}\n` +
|
|
1338
|
+
`This pairing request expires in 5 minutes. Run /pair again to generate a new one.`,
|
|
1339
|
+
);
|
|
1340
|
+
} catch (err) {
|
|
1341
|
+
h.hideSpinner();
|
|
1342
|
+
h.showError(`Pairing failed: ${err instanceof Error ? err.message : err}`);
|
|
1343
|
+
}
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1283
1347
|
if (trimmed === "/retire") {
|
|
1284
1348
|
if (!project || !zone) {
|
|
1285
1349
|
h.showError("No instance info available. Connect to a hatched instance first.");
|
package/src/index.ts
CHANGED
|
@@ -5,28 +5,40 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
import cliPkg from "../package.json";
|
|
10
|
+
import { autonomy } from "./commands/autonomy";
|
|
8
11
|
import { client } from "./commands/client";
|
|
9
12
|
import { config } from "./commands/config";
|
|
13
|
+
import { contacts } from "./commands/contacts";
|
|
10
14
|
import { email } from "./commands/email";
|
|
11
15
|
import { hatch } from "./commands/hatch";
|
|
16
|
+
import { login, logout, whoami } from "./commands/login";
|
|
12
17
|
import { ps } from "./commands/ps";
|
|
13
18
|
import { recover } from "./commands/recover";
|
|
14
19
|
import { retire } from "./commands/retire";
|
|
20
|
+
import { skills } from "./commands/skills";
|
|
15
21
|
import { sleep } from "./commands/sleep";
|
|
16
22
|
import { ssh } from "./commands/ssh";
|
|
17
23
|
import { wake } from "./commands/wake";
|
|
18
24
|
|
|
19
25
|
const commands = {
|
|
26
|
+
autonomy,
|
|
20
27
|
client,
|
|
21
28
|
config,
|
|
29
|
+
contacts,
|
|
22
30
|
email,
|
|
23
31
|
hatch,
|
|
32
|
+
login,
|
|
33
|
+
logout,
|
|
24
34
|
ps,
|
|
25
35
|
recover,
|
|
26
36
|
retire,
|
|
37
|
+
skills,
|
|
27
38
|
sleep,
|
|
28
39
|
ssh,
|
|
29
40
|
wake,
|
|
41
|
+
whoami,
|
|
30
42
|
} as const;
|
|
31
43
|
|
|
32
44
|
type CommandName = keyof typeof commands;
|
|
@@ -61,20 +73,31 @@ async function main() {
|
|
|
61
73
|
const args = process.argv.slice(2);
|
|
62
74
|
const commandName = args[0];
|
|
63
75
|
|
|
76
|
+
if (commandName === "--version" || commandName === "-v") {
|
|
77
|
+
console.log(`@vellumai/cli v${cliPkg.version}`);
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
if (!commandName || commandName === "--help" || commandName === "-h") {
|
|
65
82
|
console.log("Usage: vellum <command> [options]");
|
|
66
83
|
console.log("");
|
|
67
84
|
console.log("Commands:");
|
|
85
|
+
console.log(" autonomy View and configure autonomy tiers");
|
|
68
86
|
console.log(" client Connect to a hatched assistant");
|
|
69
87
|
console.log(" config Manage configuration");
|
|
88
|
+
console.log(" contacts Manage the contact graph");
|
|
70
89
|
console.log(" email Email operations (status, create inbox)");
|
|
71
90
|
console.log(" hatch Create a new assistant instance");
|
|
91
|
+
console.log(" login Log in to the Vellum platform");
|
|
92
|
+
console.log(" logout Log out of the Vellum platform");
|
|
72
93
|
console.log(" ps List assistants (or processes for a specific assistant)");
|
|
73
94
|
console.log(" recover Restore a previously retired local assistant");
|
|
74
95
|
console.log(" retire Delete an assistant instance");
|
|
96
|
+
console.log(" skills Browse and install skills from the Vellum catalog");
|
|
75
97
|
console.log(" sleep Stop the daemon process");
|
|
76
98
|
console.log(" ssh SSH into a remote assistant instance");
|
|
77
99
|
console.log(" wake Start the daemon and gateway");
|
|
100
|
+
console.log(" whoami Show current logged-in user");
|
|
78
101
|
process.exit(0);
|
|
79
102
|
}
|
|
80
103
|
|
package/src/lib/gcp.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { unlinkSync, writeFileSync } from "fs";
|
|
3
3
|
import { tmpdir, userInfo } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
|
|
@@ -370,13 +370,12 @@ const DESIRED_FIREWALL_RULES: FirewallRuleSpec[] = [
|
|
|
370
370
|
},
|
|
371
371
|
];
|
|
372
372
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return null;
|
|
373
|
+
import INSTALL_SCRIPT_CONTENT from "../adapters/install.sh" with { type: "text" };
|
|
374
|
+
|
|
375
|
+
function resolveInstallScriptPath(): string {
|
|
376
|
+
const tmpPath = join(tmpdir(), `vellum-install-${process.pid}.sh`);
|
|
377
|
+
writeFileSync(tmpPath, INSTALL_SCRIPT_CONTENT, { mode: 0o755 });
|
|
378
|
+
return tmpPath;
|
|
380
379
|
}
|
|
381
380
|
|
|
382
381
|
async function pollInstance(
|
|
@@ -459,11 +458,7 @@ async function recoverFromCurlFailure(
|
|
|
459
458
|
sshUser: string,
|
|
460
459
|
account?: string,
|
|
461
460
|
): Promise<void> {
|
|
462
|
-
const installScriptPath =
|
|
463
|
-
if (!installScriptPath) {
|
|
464
|
-
console.warn("\u26a0\ufe0f Skipping install script upload (not available in compiled binary)");
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
461
|
+
const installScriptPath = resolveInstallScriptPath();
|
|
467
462
|
|
|
468
463
|
const scpArgs = [
|
|
469
464
|
"compute",
|
|
@@ -488,6 +483,7 @@ async function recoverFromCurlFailure(
|
|
|
488
483
|
if (account) sshArgs.push(`--account=${account}`);
|
|
489
484
|
console.log("\ud83d\udd27 Running install script on instance...");
|
|
490
485
|
await exec("gcloud", sshArgs);
|
|
486
|
+
try { unlinkSync(installScriptPath); } catch {}
|
|
491
487
|
}
|
|
492
488
|
|
|
493
489
|
export async function hatchGcp(
|