@vellumai/cli 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -0
- package/package.json +1 -1
- package/src/adapters/openclaw.ts +7 -0
- package/src/commands/hatch.ts +119 -120
- package/src/commands/retire.ts +139 -0
- package/src/index.ts +4 -1
- package/src/lib/assistant-config.ts +95 -0
- package/src/lib/aws.ts +590 -0
- package/src/lib/gcp.ts +71 -15
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { rmSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
import { findAssistantByName, removeAssistantEntry } from "../lib/assistant-config";
|
|
7
|
+
import type { AssistantEntry } from "../lib/assistant-config";
|
|
8
|
+
import { retireInstance as retireAwsInstance } from "../lib/aws";
|
|
9
|
+
import { retireInstance as retireGcpInstance } from "../lib/gcp";
|
|
10
|
+
import { exec } from "../lib/step-runner";
|
|
11
|
+
|
|
12
|
+
function resolveCloud(entry: AssistantEntry): string {
|
|
13
|
+
if (entry.cloud) {
|
|
14
|
+
return entry.cloud;
|
|
15
|
+
}
|
|
16
|
+
if (entry.project) {
|
|
17
|
+
return "gcp";
|
|
18
|
+
}
|
|
19
|
+
if (entry.sshUser) {
|
|
20
|
+
return "custom";
|
|
21
|
+
}
|
|
22
|
+
return "local";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function extractHostFromUrl(url: string): string {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(url);
|
|
28
|
+
return parsed.hostname;
|
|
29
|
+
} catch {
|
|
30
|
+
return url.replace(/^https?:\/\//, "").split(":")[0];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function retireLocal(): Promise<void> {
|
|
35
|
+
console.log("\u{1F5D1}\ufe0f Stopping local daemon...\n");
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const child = spawn("bunx", ["vellum", "daemon", "stop"], {
|
|
39
|
+
stdio: "inherit",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await new Promise<void>((resolve) => {
|
|
43
|
+
child.on("close", () => resolve());
|
|
44
|
+
child.on("error", () => resolve());
|
|
45
|
+
});
|
|
46
|
+
} catch {}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const killGateway = spawn("pkill", ["-f", "gateway/src/index.ts"], {
|
|
50
|
+
stdio: "ignore",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await new Promise<void>((resolve) => {
|
|
54
|
+
killGateway.on("close", () => resolve());
|
|
55
|
+
killGateway.on("error", () => resolve());
|
|
56
|
+
});
|
|
57
|
+
} catch {}
|
|
58
|
+
|
|
59
|
+
const vellumDir = join(homedir(), ".vellum");
|
|
60
|
+
rmSync(vellumDir, { recursive: true, force: true });
|
|
61
|
+
console.log("\u2705 Local instance retired.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function retireCustom(entry: AssistantEntry): Promise<void> {
|
|
65
|
+
const host = extractHostFromUrl(entry.runtimeUrl);
|
|
66
|
+
const sshUser = entry.sshUser ?? "root";
|
|
67
|
+
const sshHost = `${sshUser}@${host}`;
|
|
68
|
+
|
|
69
|
+
console.log(`\u{1F5D1}\ufe0f Retiring custom instance on ${sshHost}...\n`);
|
|
70
|
+
|
|
71
|
+
const remoteCmd = [
|
|
72
|
+
"bunx vellum daemon stop 2>/dev/null || true",
|
|
73
|
+
"pkill -f gateway 2>/dev/null || true",
|
|
74
|
+
"rm -rf ~/.vellum",
|
|
75
|
+
].join(" && ");
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await exec("ssh", [
|
|
79
|
+
"-o", "StrictHostKeyChecking=no",
|
|
80
|
+
"-o", "UserKnownHostsFile=/dev/null",
|
|
81
|
+
"-o", "ConnectTimeout=10",
|
|
82
|
+
"-o", "LogLevel=ERROR",
|
|
83
|
+
sshHost,
|
|
84
|
+
remoteCmd,
|
|
85
|
+
]);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.warn(
|
|
88
|
+
`\u26a0\ufe0f Remote cleanup may have partially failed: ${error instanceof Error ? error.message : error}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`\u2705 Custom instance retired.`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function retire(): Promise<void> {
|
|
96
|
+
const name = process.argv[3];
|
|
97
|
+
|
|
98
|
+
if (!name) {
|
|
99
|
+
console.error("Error: Instance name is required.");
|
|
100
|
+
console.error("Usage: vellum-cli retire <name>");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const entry = findAssistantByName(name);
|
|
105
|
+
if (!entry) {
|
|
106
|
+
console.error(`No assistant found with name '${name}'.`);
|
|
107
|
+
console.error("Run 'vellum-cli hatch' first, or check the instance name.");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const cloud = resolveCloud(entry);
|
|
112
|
+
|
|
113
|
+
if (cloud === "gcp") {
|
|
114
|
+
const project = entry.project;
|
|
115
|
+
const zone = entry.zone;
|
|
116
|
+
if (!project || !zone) {
|
|
117
|
+
console.error("Error: GCP project and zone not found in assistant config.");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
await retireGcpInstance(name, project, zone);
|
|
121
|
+
} else if (cloud === "aws") {
|
|
122
|
+
const region = entry.region;
|
|
123
|
+
if (!region) {
|
|
124
|
+
console.error("Error: AWS region not found in assistant config.");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
await retireAwsInstance(name, region);
|
|
128
|
+
} else if (cloud === "local") {
|
|
129
|
+
await retireLocal();
|
|
130
|
+
} else if (cloud === "custom") {
|
|
131
|
+
await retireCustom(entry);
|
|
132
|
+
} else {
|
|
133
|
+
console.error(`Error: Unknown cloud type '${cloud}'.`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
removeAssistantEntry(name);
|
|
138
|
+
console.log(`Removed ${name} from config.`);
|
|
139
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { hatch } from "./commands/hatch";
|
|
4
|
+
import { retire } from "./commands/retire";
|
|
4
5
|
|
|
5
6
|
const commands = {
|
|
6
7
|
hatch,
|
|
8
|
+
retire,
|
|
7
9
|
} as const;
|
|
8
10
|
|
|
9
11
|
type CommandName = keyof typeof commands;
|
|
@@ -16,7 +18,8 @@ async function main() {
|
|
|
16
18
|
console.log("Usage: vellum-cli <command> [options]");
|
|
17
19
|
console.log("");
|
|
18
20
|
console.log("Commands:");
|
|
19
|
-
console.log(" hatch
|
|
21
|
+
console.log(" hatch Create a new assistant instance");
|
|
22
|
+
console.log(" retire Delete an assistant instance");
|
|
20
23
|
process.exit(0);
|
|
21
24
|
}
|
|
22
25
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
export interface AssistantEntry {
|
|
6
|
+
assistantId: string;
|
|
7
|
+
runtimeUrl: string;
|
|
8
|
+
bearerToken?: string;
|
|
9
|
+
cloud: string;
|
|
10
|
+
instanceId?: string;
|
|
11
|
+
project?: string;
|
|
12
|
+
region?: string;
|
|
13
|
+
species?: string;
|
|
14
|
+
sshUser?: string;
|
|
15
|
+
zone?: string;
|
|
16
|
+
hatchedAt?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface LockfileData {
|
|
20
|
+
assistants?: AssistantEntry[];
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getLockfilePath(): string {
|
|
25
|
+
return join(homedir(), ".vellum.lock.json");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readLockfile(): LockfileData {
|
|
29
|
+
const lockfilePath = getLockfilePath();
|
|
30
|
+
if (!existsSync(lockfilePath)) {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const raw = readFileSync(lockfilePath, "utf-8");
|
|
36
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
37
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
38
|
+
return parsed as LockfileData;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Malformed lockfile; return empty
|
|
42
|
+
}
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeLockfile(data: LockfileData): void {
|
|
47
|
+
const lockfilePath = getLockfilePath();
|
|
48
|
+
writeFileSync(lockfilePath, JSON.stringify(data, null, 2) + "\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readAssistants(): AssistantEntry[] {
|
|
52
|
+
const data = readLockfile();
|
|
53
|
+
const entries = data.assistants;
|
|
54
|
+
if (!Array.isArray(entries)) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
return entries.filter(
|
|
58
|
+
(e) => typeof e.assistantId === "string" && typeof e.runtimeUrl === "string",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeAssistants(entries: AssistantEntry[]): void {
|
|
63
|
+
const data = readLockfile();
|
|
64
|
+
data.assistants = entries;
|
|
65
|
+
writeLockfile(data);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function loadLatestAssistant(): AssistantEntry | null {
|
|
69
|
+
const entries = readAssistants();
|
|
70
|
+
if (entries.length === 0) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const sorted = [...entries].sort((a, b) => {
|
|
74
|
+
const ta = a.hatchedAt ? new Date(a.hatchedAt).getTime() : 0;
|
|
75
|
+
const tb = b.hatchedAt ? new Date(b.hatchedAt).getTime() : 0;
|
|
76
|
+
return tb - ta;
|
|
77
|
+
});
|
|
78
|
+
return sorted[0];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function findAssistantByName(name: string): AssistantEntry | null {
|
|
82
|
+
const entries = readAssistants();
|
|
83
|
+
return entries.find((e) => e.assistantId === name) ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function removeAssistantEntry(assistantId: string): void {
|
|
87
|
+
const entries = readAssistants();
|
|
88
|
+
writeAssistants(entries.filter((e) => e.assistantId !== assistantId));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function saveAssistantEntry(entry: AssistantEntry): void {
|
|
92
|
+
const entries = readAssistants();
|
|
93
|
+
entries.unshift(entry);
|
|
94
|
+
writeAssistants(entries);
|
|
95
|
+
}
|