@vellumai/cli 0.4.26 → 0.4.30
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 +24 -24
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +17 -5
- package/src/__tests__/retire-archive.test.ts +6 -2
- package/src/adapters/openclaw-http-server.ts +22 -7
- package/src/commands/autonomy.ts +10 -11
- package/src/commands/client.ts +25 -9
- package/src/commands/config.ts +2 -6
- package/src/commands/contacts.ts +5 -6
- package/src/commands/hatch.ts +131 -36
- package/src/commands/login.ts +6 -2
- package/src/commands/pair.ts +26 -9
- package/src/commands/ps.ts +55 -23
- package/src/commands/recover.ts +4 -2
- package/src/commands/retire.ts +59 -18
- package/src/commands/skills.ts +389 -0
- package/src/commands/sleep.ts +15 -3
- package/src/commands/ssh.ts +20 -13
- package/src/commands/tunnel.ts +6 -7
- package/src/commands/wake.ts +53 -9
- package/src/components/DefaultMainScreen.tsx +309 -99
- package/src/index.ts +5 -2
- package/src/lib/assistant-config.ts +9 -3
- package/src/lib/aws.ts +36 -11
- package/src/lib/constants.ts +3 -1
- package/src/lib/doctor-client.ts +23 -7
- package/src/lib/gcp.ts +74 -24
- package/src/lib/health-check.ts +14 -4
- package/src/lib/local.ts +249 -33
- package/src/lib/ngrok.ts +1 -3
- package/src/lib/openclaw-runtime-server.ts +7 -2
- package/src/lib/platform-client.ts +16 -3
- package/src/lib/xdg-log.ts +25 -5
package/src/commands/retire.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import { renameSync, writeFileSync } from "fs";
|
|
2
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { basename, dirname, join } from "path";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
findAssistantByName,
|
|
8
|
+
removeAssistantEntry,
|
|
9
|
+
} from "../lib/assistant-config";
|
|
7
10
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
8
11
|
import { retireInstance as retireAwsInstance } from "../lib/aws";
|
|
9
12
|
import { retireInstance as retireGcpInstance } from "../lib/gcp";
|
|
10
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
stopOrphanedDaemonProcesses,
|
|
15
|
+
stopProcessByPidFile,
|
|
16
|
+
} from "../lib/process";
|
|
11
17
|
import { getArchivePath, getMetadataPath } from "../lib/retire-archive";
|
|
12
18
|
import { exec } from "../lib/step-runner";
|
|
13
19
|
import { openLogFile, closeLogFile, writeToLogFile } from "../lib/xdg-log";
|
|
@@ -34,36 +40,57 @@ function extractHostFromUrl(url: string): string {
|
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
function getBaseDir(): string {
|
|
44
|
+
return process.env.BASE_DATA_DIR?.trim() || homedir();
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
async function retireLocal(name: string, entry: AssistantEntry): Promise<void> {
|
|
38
|
-
console.log("\u{1F5D1}\ufe0f Stopping local
|
|
48
|
+
console.log("\u{1F5D1}\ufe0f Stopping local assistant...\n");
|
|
39
49
|
|
|
40
|
-
const vellumDir = join(
|
|
50
|
+
const vellumDir = join(getBaseDir(), ".vellum");
|
|
41
51
|
|
|
42
52
|
// Stop daemon via PID file
|
|
43
53
|
const daemonPidFile = join(vellumDir, "vellum.pid");
|
|
44
54
|
const socketFile = join(vellumDir, "vellum.sock");
|
|
45
|
-
const daemonStopped = await stopProcessByPidFile(daemonPidFile, "daemon", [
|
|
55
|
+
const daemonStopped = await stopProcessByPidFile(daemonPidFile, "daemon", [
|
|
56
|
+
socketFile,
|
|
57
|
+
]);
|
|
46
58
|
|
|
47
59
|
// Stop gateway via PID file
|
|
48
60
|
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
49
61
|
await stopProcessByPidFile(gatewayPidFile, "gateway");
|
|
50
62
|
|
|
63
|
+
// Stop outbound proxy via PID file
|
|
64
|
+
const outboundProxyPidFile = join(vellumDir, "outbound-proxy.pid");
|
|
65
|
+
await stopProcessByPidFile(outboundProxyPidFile, "outbound-proxy");
|
|
66
|
+
|
|
51
67
|
// If the PID file didn't track a running daemon, scan for orphaned
|
|
52
68
|
// daemon processes that may have been started without writing a PID.
|
|
53
69
|
if (!daemonStopped) {
|
|
54
70
|
await stopOrphanedDaemonProcesses();
|
|
55
71
|
}
|
|
56
72
|
|
|
57
|
-
// Move
|
|
58
|
-
// next hatch, then kick off the tar archive in the background.
|
|
73
|
+
// Move the data directory out of the way so the path is immediately available
|
|
74
|
+
// for the next hatch, then kick off the tar archive in the background.
|
|
59
75
|
const archivePath = getArchivePath(name);
|
|
60
76
|
const metadataPath = getMetadataPath(name);
|
|
61
77
|
const stagingDir = `${archivePath}.staging`;
|
|
62
78
|
|
|
79
|
+
if (!existsSync(vellumDir)) {
|
|
80
|
+
console.log(` No data directory at ${vellumDir} — nothing to archive.`);
|
|
81
|
+
console.log("\u2705 Local instance retired.");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Ensure the retired archive directory exists before attempting the rename
|
|
86
|
+
mkdirSync(dirname(stagingDir), { recursive: true });
|
|
87
|
+
|
|
63
88
|
try {
|
|
64
89
|
renameSync(vellumDir, stagingDir);
|
|
65
90
|
} catch (err) {
|
|
66
|
-
console.warn(
|
|
91
|
+
console.warn(
|
|
92
|
+
`⚠️ Failed to move ${vellumDir}: ${err instanceof Error ? err.message : err}`,
|
|
93
|
+
);
|
|
67
94
|
console.warn("Skipping archive.");
|
|
68
95
|
console.log("\u2705 Local instance retired.");
|
|
69
96
|
return;
|
|
@@ -96,17 +123,21 @@ async function retireCustom(entry: AssistantEntry): Promise<void> {
|
|
|
96
123
|
console.log(`\u{1F5D1}\ufe0f Retiring custom instance on ${sshHost}...\n`);
|
|
97
124
|
|
|
98
125
|
const remoteCmd = [
|
|
99
|
-
"bunx vellum
|
|
126
|
+
"bunx vellum sleep 2>/dev/null || true",
|
|
100
127
|
"pkill -f gateway 2>/dev/null || true",
|
|
101
128
|
"rm -rf ~/.vellum",
|
|
102
129
|
].join(" && ");
|
|
103
130
|
|
|
104
131
|
try {
|
|
105
132
|
await exec("ssh", [
|
|
106
|
-
"-o",
|
|
107
|
-
"
|
|
108
|
-
"-o",
|
|
109
|
-
"
|
|
133
|
+
"-o",
|
|
134
|
+
"StrictHostKeyChecking=no",
|
|
135
|
+
"-o",
|
|
136
|
+
"UserKnownHostsFile=/dev/null",
|
|
137
|
+
"-o",
|
|
138
|
+
"ConnectTimeout=10",
|
|
139
|
+
"-o",
|
|
140
|
+
"LogLevel=ERROR",
|
|
110
141
|
sshHost,
|
|
111
142
|
remoteCmd,
|
|
112
143
|
]);
|
|
@@ -145,16 +176,24 @@ function teeConsoleToLogFile(fd: number | "ignore"): void {
|
|
|
145
176
|
};
|
|
146
177
|
console.warn = (...args: unknown[]) => {
|
|
147
178
|
origWarn(...args);
|
|
148
|
-
writeToLogFile(
|
|
179
|
+
writeToLogFile(
|
|
180
|
+
fd,
|
|
181
|
+
`[${timestamp()}] WARN: ${args.map(String).join(" ")}\n`,
|
|
182
|
+
);
|
|
149
183
|
};
|
|
150
184
|
console.error = (...args: unknown[]) => {
|
|
151
185
|
origError(...args);
|
|
152
|
-
writeToLogFile(
|
|
186
|
+
writeToLogFile(
|
|
187
|
+
fd,
|
|
188
|
+
`[${timestamp()}] ERROR: ${args.map(String).join(" ")}\n`,
|
|
189
|
+
);
|
|
153
190
|
};
|
|
154
191
|
}
|
|
155
192
|
|
|
156
193
|
export async function retire(): Promise<void> {
|
|
157
|
-
const logFd = process.env.VELLUM_DESKTOP_APP
|
|
194
|
+
const logFd = process.env.VELLUM_DESKTOP_APP
|
|
195
|
+
? openLogFile("retire.log")
|
|
196
|
+
: "ignore";
|
|
158
197
|
teeConsoleToLogFile(logFd);
|
|
159
198
|
|
|
160
199
|
try {
|
|
@@ -201,7 +240,9 @@ async function retireInner(): Promise<void> {
|
|
|
201
240
|
const project = entry.project;
|
|
202
241
|
const zone = entry.zone;
|
|
203
242
|
if (!project || !zone) {
|
|
204
|
-
console.error(
|
|
243
|
+
console.error(
|
|
244
|
+
"Error: GCP project and zone not found in assistant config.",
|
|
245
|
+
);
|
|
205
246
|
process.exit(1);
|
|
206
247
|
}
|
|
207
248
|
await retireGcpInstance(name, project, zone, source);
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { gunzipSync } from "node:zlib";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Path helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function getRootDir(): string {
|
|
19
|
+
return join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getSkillsDir(): string {
|
|
23
|
+
return join(getRootDir(), "workspace", "skills");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getSkillsIndexPath(): string {
|
|
27
|
+
return join(getSkillsDir(), "SKILLS.md");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Platform API client
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function getConfigPlatformUrl(): string | undefined {
|
|
35
|
+
try {
|
|
36
|
+
const configPath = join(getRootDir(), "workspace", "config.json");
|
|
37
|
+
if (!existsSync(configPath)) return undefined;
|
|
38
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8")) as Record<
|
|
39
|
+
string,
|
|
40
|
+
unknown
|
|
41
|
+
>;
|
|
42
|
+
const platform = raw.platform as Record<string, unknown> | undefined;
|
|
43
|
+
const baseUrl = platform?.baseUrl;
|
|
44
|
+
if (typeof baseUrl === "string" && baseUrl.trim()) return baseUrl.trim();
|
|
45
|
+
} catch {
|
|
46
|
+
// ignore
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getPlatformUrl(): string {
|
|
52
|
+
return (
|
|
53
|
+
process.env.VELLUM_ASSISTANT_PLATFORM_URL ??
|
|
54
|
+
getConfigPlatformUrl() ??
|
|
55
|
+
"https://platform.vellum.ai"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getPlatformToken(): string | null {
|
|
60
|
+
try {
|
|
61
|
+
return readFileSync(join(getRootDir(), "platform-token"), "utf-8").trim();
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildHeaders(): Record<string, string> {
|
|
68
|
+
const headers: Record<string, string> = {};
|
|
69
|
+
const token = getPlatformToken();
|
|
70
|
+
if (token) {
|
|
71
|
+
headers["X-Session-Token"] = token;
|
|
72
|
+
}
|
|
73
|
+
return headers;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Types
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
interface CatalogSkill {
|
|
81
|
+
id: string;
|
|
82
|
+
name: string;
|
|
83
|
+
description: string;
|
|
84
|
+
emoji?: string;
|
|
85
|
+
includes?: string[];
|
|
86
|
+
version?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface CatalogManifest {
|
|
90
|
+
version: number;
|
|
91
|
+
skills: CatalogSkill[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Catalog operations
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
async function fetchCatalog(): Promise<CatalogSkill[]> {
|
|
99
|
+
const url = `${getPlatformUrl()}/v1/skills/`;
|
|
100
|
+
const response = await fetch(url, {
|
|
101
|
+
headers: buildHeaders(),
|
|
102
|
+
signal: AbortSignal.timeout(10000),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Platform API error ${response.status}: ${response.statusText}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const manifest = (await response.json()) as CatalogManifest;
|
|
112
|
+
if (!Array.isArray(manifest.skills)) {
|
|
113
|
+
throw new Error("Platform catalog has invalid skills array");
|
|
114
|
+
}
|
|
115
|
+
return manifest.skills;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extract all files from a tar archive (uncompressed) into a directory.
|
|
120
|
+
* Returns true if a SKILL.md was found in the archive.
|
|
121
|
+
*/
|
|
122
|
+
function extractTarToDir(tarBuffer: Buffer, destDir: string): boolean {
|
|
123
|
+
let foundSkillMd = false;
|
|
124
|
+
let offset = 0;
|
|
125
|
+
while (offset + 512 <= tarBuffer.length) {
|
|
126
|
+
const header = tarBuffer.subarray(offset, offset + 512);
|
|
127
|
+
|
|
128
|
+
// End-of-archive (two consecutive zero blocks)
|
|
129
|
+
if (header.every((b) => b === 0)) break;
|
|
130
|
+
|
|
131
|
+
// Filename (bytes 0-99, null-terminated)
|
|
132
|
+
const nameEnd = header.indexOf(0, 0);
|
|
133
|
+
const name = header
|
|
134
|
+
.subarray(0, Math.min(nameEnd >= 0 ? nameEnd : 100, 100))
|
|
135
|
+
.toString("utf-8");
|
|
136
|
+
|
|
137
|
+
// File type (byte 156): '5' = directory, '0' or '\0' = regular file
|
|
138
|
+
const typeFlag = header[156];
|
|
139
|
+
|
|
140
|
+
// File size (bytes 124-135, octal)
|
|
141
|
+
const sizeStr = header.subarray(124, 136).toString("utf-8").trim();
|
|
142
|
+
const size = parseInt(sizeStr, 8) || 0;
|
|
143
|
+
|
|
144
|
+
offset += 512; // past header
|
|
145
|
+
|
|
146
|
+
// Skip directories and empty names
|
|
147
|
+
if (name && typeFlag !== 53 /* '5' */) {
|
|
148
|
+
// Prevent path traversal
|
|
149
|
+
const normalizedName = name.replace(/^\.\//, "");
|
|
150
|
+
if (!normalizedName.startsWith("..") && !normalizedName.includes("/..")) {
|
|
151
|
+
const destPath = join(destDir, normalizedName);
|
|
152
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
153
|
+
writeFileSync(destPath, tarBuffer.subarray(offset, offset + size));
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
normalizedName === "SKILL.md" ||
|
|
157
|
+
normalizedName.endsWith("/SKILL.md")
|
|
158
|
+
) {
|
|
159
|
+
foundSkillMd = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Skip to next header (data padded to 512 bytes)
|
|
165
|
+
offset += Math.ceil(size / 512) * 512;
|
|
166
|
+
}
|
|
167
|
+
return foundSkillMd;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function fetchAndExtractSkill(
|
|
171
|
+
skillId: string,
|
|
172
|
+
destDir: string,
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
const url = `${getPlatformUrl()}/v1/skills/${encodeURIComponent(skillId)}/`;
|
|
175
|
+
const response = await fetch(url, {
|
|
176
|
+
headers: buildHeaders(),
|
|
177
|
+
signal: AbortSignal.timeout(15000),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Failed to fetch skill "${skillId}": HTTP ${response.status}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const gzipBuffer = Buffer.from(await response.arrayBuffer());
|
|
187
|
+
const tarBuffer = gunzipSync(gzipBuffer);
|
|
188
|
+
const foundSkillMd = extractTarToDir(tarBuffer, destDir);
|
|
189
|
+
|
|
190
|
+
if (!foundSkillMd) {
|
|
191
|
+
throw new Error(`SKILL.md not found in archive for "${skillId}"`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Managed skill installation
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
function atomicWriteFile(filePath: string, content: string): void {
|
|
200
|
+
const dir = dirname(filePath);
|
|
201
|
+
mkdirSync(dir, { recursive: true });
|
|
202
|
+
const tmpPath = join(dir, `.tmp-${randomUUID()}`);
|
|
203
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
204
|
+
renameSync(tmpPath, filePath);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function upsertSkillsIndex(id: string): void {
|
|
208
|
+
const indexPath = getSkillsIndexPath();
|
|
209
|
+
let lines: string[] = [];
|
|
210
|
+
if (existsSync(indexPath)) {
|
|
211
|
+
lines = readFileSync(indexPath, "utf-8").split("\n");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
215
|
+
const pattern = new RegExp(`^[-*]\\s+(?:\`)?${escaped}(?:\`)?\\s*$`);
|
|
216
|
+
if (lines.some((line) => pattern.test(line))) return;
|
|
217
|
+
|
|
218
|
+
const nonEmpty = lines.filter((l) => l.trim());
|
|
219
|
+
nonEmpty.push(`- ${id}`);
|
|
220
|
+
const content = nonEmpty.join("\n");
|
|
221
|
+
atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function installSkillLocally(
|
|
225
|
+
skillId: string,
|
|
226
|
+
catalogEntry: CatalogSkill,
|
|
227
|
+
overwrite: boolean,
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
const skillDir = join(getSkillsDir(), skillId);
|
|
230
|
+
const skillFilePath = join(skillDir, "SKILL.md");
|
|
231
|
+
|
|
232
|
+
if (existsSync(skillFilePath) && !overwrite) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Skill "${skillId}" is already installed. Use --overwrite to replace it.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
mkdirSync(skillDir, { recursive: true });
|
|
239
|
+
|
|
240
|
+
// Extract all files from the archive into the skill directory
|
|
241
|
+
await fetchAndExtractSkill(skillId, skillDir);
|
|
242
|
+
|
|
243
|
+
// Write version metadata
|
|
244
|
+
if (catalogEntry.version) {
|
|
245
|
+
const meta = {
|
|
246
|
+
version: catalogEntry.version,
|
|
247
|
+
installedAt: new Date().toISOString(),
|
|
248
|
+
};
|
|
249
|
+
atomicWriteFile(
|
|
250
|
+
join(skillDir, "version.json"),
|
|
251
|
+
JSON.stringify(meta, null, 2) + "\n",
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Install npm dependencies if the skill has a package.json
|
|
256
|
+
if (existsSync(join(skillDir, "package.json"))) {
|
|
257
|
+
const bunPath = `${process.env.HOME || "/root"}/.bun/bin`;
|
|
258
|
+
execSync("bun install", {
|
|
259
|
+
cwd: skillDir,
|
|
260
|
+
stdio: "inherit",
|
|
261
|
+
env: { ...process.env, PATH: `${bunPath}:${process.env.PATH}` },
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Register in SKILLS.md only after all steps succeed
|
|
266
|
+
upsertSkillsIndex(skillId);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Helpers
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
function hasFlag(args: string[], flag: string): boolean {
|
|
274
|
+
return args.includes(flag);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// Usage
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
function printUsage(): void {
|
|
282
|
+
console.log("Usage: vellum skills <subcommand> [options]");
|
|
283
|
+
console.log("");
|
|
284
|
+
console.log("Subcommands:");
|
|
285
|
+
console.log(
|
|
286
|
+
" list List available catalog skills",
|
|
287
|
+
);
|
|
288
|
+
console.log(
|
|
289
|
+
" install <skill-id> [--overwrite] Install a skill from the catalog",
|
|
290
|
+
);
|
|
291
|
+
console.log("");
|
|
292
|
+
console.log("Options:");
|
|
293
|
+
console.log(" --json Machine-readable JSON output");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// Command entry point
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
export async function skills(): Promise<void> {
|
|
301
|
+
const args = process.argv.slice(3);
|
|
302
|
+
const subcommand = args[0];
|
|
303
|
+
const json = hasFlag(args, "--json");
|
|
304
|
+
|
|
305
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
306
|
+
printUsage();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
switch (subcommand) {
|
|
311
|
+
case "list": {
|
|
312
|
+
try {
|
|
313
|
+
const catalog = await fetchCatalog();
|
|
314
|
+
|
|
315
|
+
if (json) {
|
|
316
|
+
console.log(JSON.stringify({ ok: true, skills: catalog }));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (catalog.length === 0) {
|
|
321
|
+
console.log("No skills available in the catalog.");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
console.log(`Available skills (${catalog.length}):\n`);
|
|
326
|
+
for (const s of catalog) {
|
|
327
|
+
const emoji = s.emoji ? `${s.emoji} ` : "";
|
|
328
|
+
const deps = s.includes?.length
|
|
329
|
+
? ` (requires: ${s.includes.join(", ")})`
|
|
330
|
+
: "";
|
|
331
|
+
console.log(` ${emoji}${s.id}`);
|
|
332
|
+
console.log(` ${s.name} — ${s.description}${deps}`);
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
335
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
336
|
+
if (json) {
|
|
337
|
+
console.log(JSON.stringify({ ok: false, error: msg }));
|
|
338
|
+
} else {
|
|
339
|
+
console.error(`Error: ${msg}`);
|
|
340
|
+
}
|
|
341
|
+
process.exitCode = 1;
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
case "install": {
|
|
347
|
+
const skillId = args.find((a) => !a.startsWith("--") && a !== "install");
|
|
348
|
+
if (!skillId) {
|
|
349
|
+
console.error("Usage: vellum skills install <skill-id>");
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const overwrite = hasFlag(args, "--overwrite");
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
// Verify skill exists in catalog
|
|
357
|
+
const catalog = await fetchCatalog();
|
|
358
|
+
const entry = catalog.find((s) => s.id === skillId);
|
|
359
|
+
if (!entry) {
|
|
360
|
+
throw new Error(`Skill "${skillId}" not found in the Vellum catalog`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Fetch, extract, and install
|
|
364
|
+
await installSkillLocally(skillId, entry, overwrite);
|
|
365
|
+
|
|
366
|
+
if (json) {
|
|
367
|
+
console.log(JSON.stringify({ ok: true, skillId }));
|
|
368
|
+
} else {
|
|
369
|
+
console.log(`Installed skill "${skillId}".`);
|
|
370
|
+
}
|
|
371
|
+
} catch (err) {
|
|
372
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
373
|
+
if (json) {
|
|
374
|
+
console.log(JSON.stringify({ ok: false, error: msg }));
|
|
375
|
+
} else {
|
|
376
|
+
console.error(`Error: ${msg}`);
|
|
377
|
+
}
|
|
378
|
+
process.exitCode = 1;
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
default: {
|
|
384
|
+
console.error(`Unknown skills subcommand: ${subcommand}`);
|
|
385
|
+
printUsage();
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
package/src/commands/sleep.ts
CHANGED
|
@@ -8,7 +8,7 @@ export async function sleep(): Promise<void> {
|
|
|
8
8
|
if (args.includes("--help") || args.includes("-h")) {
|
|
9
9
|
console.log("Usage: vellum sleep");
|
|
10
10
|
console.log("");
|
|
11
|
-
console.log("Stop the
|
|
11
|
+
console.log("Stop the assistant, gateway, and outbound-proxy processes.");
|
|
12
12
|
process.exit(0);
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -16,15 +16,16 @@ export async function sleep(): Promise<void> {
|
|
|
16
16
|
const daemonPidFile = join(vellumDir, "vellum.pid");
|
|
17
17
|
const socketFile = join(vellumDir, "vellum.sock");
|
|
18
18
|
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
19
|
+
const outboundProxyPidFile = join(vellumDir, "outbound-proxy.pid");
|
|
19
20
|
|
|
20
21
|
// Stop daemon
|
|
21
22
|
const daemonStopped = await stopProcessByPidFile(daemonPidFile, "daemon", [
|
|
22
23
|
socketFile,
|
|
23
24
|
]);
|
|
24
25
|
if (!daemonStopped) {
|
|
25
|
-
console.log("
|
|
26
|
+
console.log("Assistant is not running.");
|
|
26
27
|
} else {
|
|
27
|
-
console.log("
|
|
28
|
+
console.log("Assistant stopped.");
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
// Stop gateway
|
|
@@ -34,4 +35,15 @@ export async function sleep(): Promise<void> {
|
|
|
34
35
|
} else {
|
|
35
36
|
console.log("Gateway stopped.");
|
|
36
37
|
}
|
|
38
|
+
|
|
39
|
+
// Stop outbound proxy
|
|
40
|
+
const outboundProxyStopped = await stopProcessByPidFile(
|
|
41
|
+
outboundProxyPidFile,
|
|
42
|
+
"outbound-proxy",
|
|
43
|
+
);
|
|
44
|
+
if (!outboundProxyStopped) {
|
|
45
|
+
console.log("Outbound proxy is not running.");
|
|
46
|
+
} else {
|
|
47
|
+
console.log("Outbound proxy stopped.");
|
|
48
|
+
}
|
|
37
49
|
}
|
package/src/commands/ssh.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
findAssistantByName,
|
|
5
|
+
loadLatestAssistant,
|
|
6
|
+
} from "../lib/assistant-config";
|
|
4
7
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
5
8
|
|
|
6
9
|
const SSH_OPTS = [
|
|
7
|
-
"-o",
|
|
8
|
-
"
|
|
9
|
-
"-o",
|
|
10
|
-
"
|
|
10
|
+
"-o",
|
|
11
|
+
"StrictHostKeyChecking=no",
|
|
12
|
+
"-o",
|
|
13
|
+
"UserKnownHostsFile=/dev/null",
|
|
14
|
+
"-o",
|
|
15
|
+
"ConnectTimeout=10",
|
|
16
|
+
"-o",
|
|
17
|
+
"LogLevel=ERROR",
|
|
11
18
|
];
|
|
12
19
|
|
|
13
20
|
function resolveCloud(entry: AssistantEntry): string {
|
|
@@ -40,7 +47,9 @@ export async function ssh(): Promise<void> {
|
|
|
40
47
|
console.log("SSH into a remote assistant instance.");
|
|
41
48
|
console.log("");
|
|
42
49
|
console.log("Arguments:");
|
|
43
|
-
console.log(
|
|
50
|
+
console.log(
|
|
51
|
+
" <name> Name of the assistant to connect to (defaults to latest)",
|
|
52
|
+
);
|
|
44
53
|
process.exit(0);
|
|
45
54
|
}
|
|
46
55
|
|
|
@@ -61,7 +70,7 @@ export async function ssh(): Promise<void> {
|
|
|
61
70
|
if (cloud === "local") {
|
|
62
71
|
console.error(
|
|
63
72
|
"Cannot SSH into a local assistant. Local assistants run directly on this machine.\n" +
|
|
64
|
-
|
|
73
|
+
`Use 'vellum ps ${entry.assistantId}' to check its processes instead.`,
|
|
65
74
|
);
|
|
66
75
|
process.exit(1);
|
|
67
76
|
}
|
|
@@ -77,7 +86,9 @@ export async function ssh(): Promise<void> {
|
|
|
77
86
|
const project = entry.project;
|
|
78
87
|
const zone = entry.zone;
|
|
79
88
|
if (!project || !zone) {
|
|
80
|
-
console.error(
|
|
89
|
+
console.error(
|
|
90
|
+
"Error: GCP project and zone not found in assistant config.",
|
|
91
|
+
);
|
|
81
92
|
process.exit(1);
|
|
82
93
|
}
|
|
83
94
|
|
|
@@ -102,11 +113,7 @@ export async function ssh(): Promise<void> {
|
|
|
102
113
|
|
|
103
114
|
console.log(`🔗 Connecting to ${entry.assistantId} via ssh...\n`);
|
|
104
115
|
|
|
105
|
-
child = spawn(
|
|
106
|
-
"ssh",
|
|
107
|
-
[...SSH_OPTS, sshTarget],
|
|
108
|
-
{ stdio: "inherit" },
|
|
109
|
-
);
|
|
116
|
+
child = spawn("ssh", [...SSH_OPTS, sshTarget], { stdio: "inherit" });
|
|
110
117
|
} else {
|
|
111
118
|
console.error(`Error: Unknown cloud type '${cloud}'.`);
|
|
112
119
|
process.exit(1);
|
package/src/commands/tunnel.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
findAssistantByName,
|
|
3
|
+
loadLatestAssistant,
|
|
4
|
+
} from "../lib/assistant-config";
|
|
2
5
|
import { runNgrokTunnel } from "../lib/ngrok";
|
|
3
6
|
|
|
4
7
|
const VALID_PROVIDERS = ["vellum", "ngrok", "cloudflare", "tailscale"] as const;
|
|
@@ -70,9 +73,7 @@ export async function tunnel(): Promise<void> {
|
|
|
70
73
|
`No assistant instance found with name '${assistantName}'.`,
|
|
71
74
|
);
|
|
72
75
|
} else {
|
|
73
|
-
console.error(
|
|
74
|
-
"No assistant instance found. Run `vellum hatch` first.",
|
|
75
|
-
);
|
|
76
|
+
console.error("No assistant instance found. Run `vellum hatch` first.");
|
|
76
77
|
}
|
|
77
78
|
process.exit(1);
|
|
78
79
|
}
|
|
@@ -82,7 +83,5 @@ export async function tunnel(): Promise<void> {
|
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
throw new Error(
|
|
86
|
-
`Tunnel provider '${provider}' is not yet implemented.`,
|
|
87
|
-
);
|
|
86
|
+
throw new Error(`Tunnel provider '${provider}' is not yet implemented.`);
|
|
88
87
|
}
|