@vellumai/cli 0.4.29 → 0.4.31
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/package.json +1 -1
- package/src/commands/contacts.ts +6 -10
- package/src/commands/hatch.ts +0 -3
- package/src/commands/ps.ts +0 -8
- package/src/commands/retire.ts +17 -8
- package/src/commands/skills.ts +389 -0
- package/src/commands/sleep.ts +1 -12
- package/src/commands/wake.ts +33 -14
- package/src/index.ts +3 -0
- package/src/lib/local.ts +3 -152
package/package.json
CHANGED
package/src/commands/contacts.ts
CHANGED
|
@@ -82,10 +82,7 @@ interface ContactChannel {
|
|
|
82
82
|
interface Contact {
|
|
83
83
|
id: string;
|
|
84
84
|
displayName: string;
|
|
85
|
-
|
|
86
|
-
importance: number;
|
|
87
|
-
responseExpectation: string | null;
|
|
88
|
-
preferredTone: string | null;
|
|
85
|
+
notes: string | null;
|
|
89
86
|
lastInteraction: number | null;
|
|
90
87
|
interactionCount: number;
|
|
91
88
|
channels: ContactChannel[];
|
|
@@ -109,10 +106,7 @@ function formatContact(c: Contact): string {
|
|
|
109
106
|
const lines = [
|
|
110
107
|
` ID: ${c.id}`,
|
|
111
108
|
` Name: ${c.displayName}`,
|
|
112
|
-
`
|
|
113
|
-
` Importance: ${c.importance.toFixed(2)}`,
|
|
114
|
-
` Response: ${c.responseExpectation ?? "(none)"}`,
|
|
115
|
-
` Tone: ${c.preferredTone ?? "(none)"}`,
|
|
109
|
+
` Notes: ${c.notes ?? "(none)"}`,
|
|
116
110
|
` Interactions: ${c.interactionCount}`,
|
|
117
111
|
];
|
|
118
112
|
if (c.lastInteraction) {
|
|
@@ -136,7 +130,7 @@ function printUsage(): void {
|
|
|
136
130
|
console.log("Usage: vellum contacts <subcommand> [options]");
|
|
137
131
|
console.log("");
|
|
138
132
|
console.log("Subcommands:");
|
|
139
|
-
console.log(" list [--limit N]
|
|
133
|
+
console.log(" list [--limit N] [--role ROLE] List all contacts");
|
|
140
134
|
console.log(" get <id> Get a contact by ID");
|
|
141
135
|
console.log(" merge <keepId> <mergeId> Merge two contacts");
|
|
142
136
|
console.log("");
|
|
@@ -161,7 +155,9 @@ export async function contacts(): Promise<void> {
|
|
|
161
155
|
switch (subcommand) {
|
|
162
156
|
case "list": {
|
|
163
157
|
const limit = getFlagValue(args, "--limit") ?? "50";
|
|
164
|
-
const
|
|
158
|
+
const role = getFlagValue(args, "--role");
|
|
159
|
+
const query = `contacts?limit=${limit}${role ? `&role=${encodeURIComponent(role)}` : ""}`;
|
|
160
|
+
const data = (await apiGet(query)) as {
|
|
165
161
|
ok: boolean;
|
|
166
162
|
contacts: Contact[];
|
|
167
163
|
};
|
package/src/commands/hatch.ts
CHANGED
|
@@ -39,7 +39,6 @@ import type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
|
39
39
|
import {
|
|
40
40
|
startLocalDaemon,
|
|
41
41
|
startGateway,
|
|
42
|
-
startOutboundProxy,
|
|
43
42
|
stopLocalProcesses,
|
|
44
43
|
} from "../lib/local";
|
|
45
44
|
import { probePort } from "../lib/port-probe";
|
|
@@ -757,8 +756,6 @@ async function hatchLocal(
|
|
|
757
756
|
throw error;
|
|
758
757
|
}
|
|
759
758
|
|
|
760
|
-
await startOutboundProxy(watch);
|
|
761
|
-
|
|
762
759
|
// Read the bearer token written by the daemon so the client can authenticate
|
|
763
760
|
// with the gateway (which requires auth by default).
|
|
764
761
|
let bearerToken: string | undefined;
|
package/src/commands/ps.ts
CHANGED
|
@@ -220,8 +220,6 @@ function formatDetectionInfo(proc: DetectedProcess): string {
|
|
|
220
220
|
async function getLocalProcesses(entry: AssistantEntry): Promise<TableRow[]> {
|
|
221
221
|
const vellumDir = entry.baseDataDir ?? join(homedir(), ".vellum");
|
|
222
222
|
|
|
223
|
-
const PROXY_PORT = Number(process.env.PROXY_PORT) || 7829;
|
|
224
|
-
|
|
225
223
|
const specs: ProcessSpec[] = [
|
|
226
224
|
{
|
|
227
225
|
name: "assistant",
|
|
@@ -241,12 +239,6 @@ async function getLocalProcesses(entry: AssistantEntry): Promise<TableRow[]> {
|
|
|
241
239
|
port: GATEWAY_PORT,
|
|
242
240
|
pidFile: join(vellumDir, "gateway.pid"),
|
|
243
241
|
},
|
|
244
|
-
{
|
|
245
|
-
name: "outbound-proxy",
|
|
246
|
-
pgrepName: "outbound-proxy",
|
|
247
|
-
port: PROXY_PORT,
|
|
248
|
-
pidFile: join(vellumDir, "outbound-proxy.pid"),
|
|
249
|
-
},
|
|
250
242
|
{
|
|
251
243
|
name: "embed-worker",
|
|
252
244
|
pgrepName: "embed-worker",
|
package/src/commands/retire.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
|
|
|
@@ -40,10 +40,14 @@ function extractHostFromUrl(url: string): string {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function getBaseDir(): string {
|
|
44
|
+
return process.env.BASE_DATA_DIR?.trim() || homedir();
|
|
45
|
+
}
|
|
46
|
+
|
|
43
47
|
async function retireLocal(name: string, entry: AssistantEntry): Promise<void> {
|
|
44
48
|
console.log("\u{1F5D1}\ufe0f Stopping local assistant...\n");
|
|
45
49
|
|
|
46
|
-
const vellumDir = join(
|
|
50
|
+
const vellumDir = join(getBaseDir(), ".vellum");
|
|
47
51
|
|
|
48
52
|
// Stop daemon via PID file
|
|
49
53
|
const daemonPidFile = join(vellumDir, "vellum.pid");
|
|
@@ -56,22 +60,27 @@ async function retireLocal(name: string, entry: AssistantEntry): Promise<void> {
|
|
|
56
60
|
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
57
61
|
await stopProcessByPidFile(gatewayPidFile, "gateway");
|
|
58
62
|
|
|
59
|
-
// Stop outbound proxy via PID file
|
|
60
|
-
const outboundProxyPidFile = join(vellumDir, "outbound-proxy.pid");
|
|
61
|
-
await stopProcessByPidFile(outboundProxyPidFile, "outbound-proxy");
|
|
62
|
-
|
|
63
63
|
// If the PID file didn't track a running daemon, scan for orphaned
|
|
64
64
|
// daemon processes that may have been started without writing a PID.
|
|
65
65
|
if (!daemonStopped) {
|
|
66
66
|
await stopOrphanedDaemonProcesses();
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
// Move
|
|
70
|
-
// next hatch, then kick off the tar archive in the background.
|
|
69
|
+
// Move the data directory out of the way so the path is immediately available
|
|
70
|
+
// for the next hatch, then kick off the tar archive in the background.
|
|
71
71
|
const archivePath = getArchivePath(name);
|
|
72
72
|
const metadataPath = getMetadataPath(name);
|
|
73
73
|
const stagingDir = `${archivePath}.staging`;
|
|
74
74
|
|
|
75
|
+
if (!existsSync(vellumDir)) {
|
|
76
|
+
console.log(` No data directory at ${vellumDir} — nothing to archive.`);
|
|
77
|
+
console.log("\u2705 Local instance retired.");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Ensure the retired archive directory exists before attempting the rename
|
|
82
|
+
mkdirSync(dirname(stagingDir), { recursive: true });
|
|
83
|
+
|
|
75
84
|
try {
|
|
76
85
|
renameSync(vellumDir, stagingDir);
|
|
77
86
|
} catch (err) {
|
|
@@ -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 assistant
|
|
11
|
+
console.log("Stop the assistant and gateway processes.");
|
|
12
12
|
process.exit(0);
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -16,7 +16,6 @@ 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");
|
|
20
19
|
|
|
21
20
|
// Stop daemon
|
|
22
21
|
const daemonStopped = await stopProcessByPidFile(daemonPidFile, "daemon", [
|
|
@@ -36,14 +35,4 @@ export async function sleep(): Promise<void> {
|
|
|
36
35
|
console.log("Gateway stopped.");
|
|
37
36
|
}
|
|
38
37
|
|
|
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
|
-
}
|
|
49
38
|
}
|
package/src/commands/wake.ts
CHANGED
|
@@ -3,22 +3,25 @@ import { homedir } from "os";
|
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
|
|
5
5
|
import { loadAllAssistants } from "../lib/assistant-config";
|
|
6
|
-
import { isProcessAlive } from "../lib/process";
|
|
7
|
-
import {
|
|
8
|
-
startLocalDaemon,
|
|
9
|
-
startGateway,
|
|
10
|
-
startOutboundProxy,
|
|
11
|
-
} from "../lib/local";
|
|
6
|
+
import { isProcessAlive, stopProcessByPidFile } from "../lib/process";
|
|
7
|
+
import { startLocalDaemon, startGateway } from "../lib/local";
|
|
12
8
|
|
|
13
9
|
export async function wake(): Promise<void> {
|
|
14
10
|
const args = process.argv.slice(3);
|
|
15
11
|
if (args.includes("--help") || args.includes("-h")) {
|
|
16
|
-
console.log("Usage: vellum wake");
|
|
12
|
+
console.log("Usage: vellum wake [options]");
|
|
17
13
|
console.log("");
|
|
18
14
|
console.log("Start the assistant and gateway processes.");
|
|
15
|
+
console.log("");
|
|
16
|
+
console.log("Options:");
|
|
17
|
+
console.log(
|
|
18
|
+
" --watch Run assistant and gateway in watch mode (hot reload on source changes)",
|
|
19
|
+
);
|
|
19
20
|
process.exit(0);
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
const watch = args.includes("--watch");
|
|
24
|
+
|
|
22
25
|
const assistants = loadAllAssistants();
|
|
23
26
|
const hasLocal = assistants.some((a) => a.cloud === "local");
|
|
24
27
|
if (!hasLocal) {
|
|
@@ -30,6 +33,7 @@ export async function wake(): Promise<void> {
|
|
|
30
33
|
|
|
31
34
|
const vellumDir = join(homedir(), ".vellum");
|
|
32
35
|
const pidFile = join(vellumDir, "vellum.pid");
|
|
36
|
+
const socketFile = join(vellumDir, "vellum.sock");
|
|
33
37
|
|
|
34
38
|
// Check if daemon is already running
|
|
35
39
|
let daemonRunning = false;
|
|
@@ -40,7 +44,16 @@ export async function wake(): Promise<void> {
|
|
|
40
44
|
try {
|
|
41
45
|
process.kill(pid, 0);
|
|
42
46
|
daemonRunning = true;
|
|
43
|
-
|
|
47
|
+
if (watch) {
|
|
48
|
+
// Restart in watch mode
|
|
49
|
+
console.log(
|
|
50
|
+
`Assistant running (pid ${pid}) — restarting in watch mode...`,
|
|
51
|
+
);
|
|
52
|
+
await stopProcessByPidFile(pidFile, "assistant", [socketFile]);
|
|
53
|
+
daemonRunning = false;
|
|
54
|
+
} else {
|
|
55
|
+
console.log(`Assistant already running (pid ${pid}).`);
|
|
56
|
+
}
|
|
44
57
|
} catch {
|
|
45
58
|
// Process not alive, will start below
|
|
46
59
|
}
|
|
@@ -48,7 +61,7 @@ export async function wake(): Promise<void> {
|
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
if (!daemonRunning) {
|
|
51
|
-
await startLocalDaemon();
|
|
64
|
+
await startLocalDaemon(watch);
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
// Start gateway (non-desktop only)
|
|
@@ -56,14 +69,20 @@ export async function wake(): Promise<void> {
|
|
|
56
69
|
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
57
70
|
const { alive, pid } = isProcessAlive(gatewayPidFile);
|
|
58
71
|
if (alive) {
|
|
59
|
-
|
|
72
|
+
if (watch) {
|
|
73
|
+
// Restart in watch mode
|
|
74
|
+
console.log(
|
|
75
|
+
`Gateway running (pid ${pid}) — restarting in watch mode...`,
|
|
76
|
+
);
|
|
77
|
+
await stopProcessByPidFile(gatewayPidFile, "gateway");
|
|
78
|
+
await startGateway(undefined, watch);
|
|
79
|
+
} else {
|
|
80
|
+
console.log(`Gateway already running (pid ${pid}).`);
|
|
81
|
+
}
|
|
60
82
|
} else {
|
|
61
|
-
await startGateway();
|
|
83
|
+
await startGateway(undefined, watch);
|
|
62
84
|
}
|
|
63
85
|
}
|
|
64
86
|
|
|
65
|
-
// Start outbound proxy
|
|
66
|
-
await startOutboundProxy();
|
|
67
|
-
|
|
68
87
|
console.log("✅ Wake complete.");
|
|
69
88
|
}
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { pair } from "./commands/pair";
|
|
|
18
18
|
import { ps } from "./commands/ps";
|
|
19
19
|
import { recover } from "./commands/recover";
|
|
20
20
|
import { retire } from "./commands/retire";
|
|
21
|
+
import { skills } from "./commands/skills";
|
|
21
22
|
import { sleep } from "./commands/sleep";
|
|
22
23
|
import { ssh } from "./commands/ssh";
|
|
23
24
|
import { tunnel } from "./commands/tunnel";
|
|
@@ -36,6 +37,7 @@ const commands = {
|
|
|
36
37
|
ps,
|
|
37
38
|
recover,
|
|
38
39
|
retire,
|
|
40
|
+
skills,
|
|
39
41
|
sleep,
|
|
40
42
|
ssh,
|
|
41
43
|
tunnel,
|
|
@@ -97,6 +99,7 @@ async function main() {
|
|
|
97
99
|
);
|
|
98
100
|
console.log(" recover Restore a previously retired local assistant");
|
|
99
101
|
console.log(" retire Delete an assistant instance");
|
|
102
|
+
console.log(" skills Browse and install skills from the Vellum catalog");
|
|
100
103
|
console.log(" sleep Stop the assistant process");
|
|
101
104
|
console.log(" ssh SSH into a remote assistant instance");
|
|
102
105
|
console.log(" tunnel Create a tunnel for a locally hosted assistant");
|
package/src/lib/local.ts
CHANGED
|
@@ -79,18 +79,6 @@ function findGatewaySourceFromCwd(): string | undefined {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function isOutboundProxySourceDir(dir: string): boolean {
|
|
83
|
-
const pkgPath = join(dir, "package.json");
|
|
84
|
-
if (!existsSync(pkgPath) || !existsSync(join(dir, "src", "main.ts")))
|
|
85
|
-
return false;
|
|
86
|
-
try {
|
|
87
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
88
|
-
return pkg.name === "@vellumai/outbound-proxy";
|
|
89
|
-
} catch {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
82
|
function resolveAssistantIndexPath(): string | undefined {
|
|
95
83
|
// Source tree layout: cli/src/lib/ -> ../../.. -> repo root -> assistant/src/index.ts
|
|
96
84
|
const sourceTreeIndex = join(
|
|
@@ -354,21 +342,6 @@ function resolveGatewayDir(): string {
|
|
|
354
342
|
}
|
|
355
343
|
}
|
|
356
344
|
|
|
357
|
-
function resolveOutboundProxyDir(): string | undefined {
|
|
358
|
-
// Compiled binary: outbound-proxy/ bundled adjacent to the CLI executable.
|
|
359
|
-
const binProxy = join(dirname(process.execPath), "outbound-proxy");
|
|
360
|
-
if (isOutboundProxySourceDir(binProxy)) {
|
|
361
|
-
return binProxy;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
const pkgPath = _require.resolve("@vellumai/outbound-proxy/package.json");
|
|
366
|
-
return dirname(pkgPath);
|
|
367
|
-
} catch {
|
|
368
|
-
return undefined;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
345
|
function normalizeIngressUrl(value: unknown): string | undefined {
|
|
373
346
|
if (typeof value !== "string") return undefined;
|
|
374
347
|
const normalized = value.trim().replace(/\/+$/, "");
|
|
@@ -949,130 +922,10 @@ export async function startGateway(
|
|
|
949
922
|
return gatewayUrl;
|
|
950
923
|
}
|
|
951
924
|
|
|
952
|
-
export async function startOutboundProxy(
|
|
953
|
-
watch: boolean = false,
|
|
954
|
-
): Promise<void> {
|
|
955
|
-
const proxyDir = resolveOutboundProxyDir();
|
|
956
|
-
if (!proxyDir) {
|
|
957
|
-
console.log(" ⚠️ Outbound proxy not found — skipping");
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
console.log("🔒 Starting outbound proxy...");
|
|
962
|
-
|
|
963
|
-
const vellumDir = join(homedir(), ".vellum");
|
|
964
|
-
mkdirSync(vellumDir, { recursive: true });
|
|
965
|
-
|
|
966
|
-
const pidFile = join(vellumDir, "outbound-proxy.pid");
|
|
967
|
-
|
|
968
|
-
// Check if already running
|
|
969
|
-
if (existsSync(pidFile)) {
|
|
970
|
-
try {
|
|
971
|
-
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
972
|
-
if (!isNaN(pid)) {
|
|
973
|
-
try {
|
|
974
|
-
process.kill(pid, 0);
|
|
975
|
-
console.log(` Outbound proxy already running (pid ${pid})\n`);
|
|
976
|
-
return;
|
|
977
|
-
} catch {
|
|
978
|
-
try {
|
|
979
|
-
unlinkSync(pidFile);
|
|
980
|
-
} catch {}
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
} catch {}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
const proxyEnv: Record<string, string> = {
|
|
987
|
-
...(process.env as Record<string, string>),
|
|
988
|
-
PROXY_PORT: process.env.PROXY_PORT || "7829",
|
|
989
|
-
PROXY_HEALTH_PORT: process.env.PROXY_HEALTH_PORT || "7828",
|
|
990
|
-
};
|
|
991
|
-
|
|
992
|
-
const proxyLogFd = openLogFile("hatch.log");
|
|
993
|
-
|
|
994
|
-
let proxy;
|
|
995
|
-
if (process.env.VELLUM_DESKTOP_APP && !watch) {
|
|
996
|
-
const proxyBinary = join(
|
|
997
|
-
dirname(process.execPath),
|
|
998
|
-
"vellum-outbound-proxy",
|
|
999
|
-
);
|
|
1000
|
-
if (!existsSync(proxyBinary)) {
|
|
1001
|
-
console.log(
|
|
1002
|
-
" ⚠️ Outbound proxy binary not found — falling back to source",
|
|
1003
|
-
);
|
|
1004
|
-
const bunArgs = watch
|
|
1005
|
-
? ["--watch", "run", "src/main.ts"]
|
|
1006
|
-
: ["run", "src/main.ts"];
|
|
1007
|
-
proxy = spawn("bun", bunArgs, {
|
|
1008
|
-
cwd: proxyDir,
|
|
1009
|
-
detached: true,
|
|
1010
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1011
|
-
env: proxyEnv,
|
|
1012
|
-
});
|
|
1013
|
-
} else {
|
|
1014
|
-
proxy = spawn(proxyBinary, [], {
|
|
1015
|
-
detached: true,
|
|
1016
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1017
|
-
env: proxyEnv,
|
|
1018
|
-
});
|
|
1019
|
-
}
|
|
1020
|
-
} else {
|
|
1021
|
-
const bunArgs = watch
|
|
1022
|
-
? ["--watch", "run", "src/main.ts"]
|
|
1023
|
-
: ["run", "src/main.ts"];
|
|
1024
|
-
proxy = spawn("bun", bunArgs, {
|
|
1025
|
-
cwd: proxyDir,
|
|
1026
|
-
detached: true,
|
|
1027
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1028
|
-
env: proxyEnv,
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
pipeToLogFile(proxy, proxyLogFd, "outbound-proxy");
|
|
1033
|
-
proxy.unref();
|
|
1034
|
-
|
|
1035
|
-
if (proxy.pid) {
|
|
1036
|
-
writeFileSync(pidFile, String(proxy.pid), "utf-8");
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
if (watch) {
|
|
1040
|
-
console.log(" Outbound proxy started in watch mode (bun --watch)");
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
// Wait for the health endpoint to respond
|
|
1044
|
-
const healthPort = Number(process.env.PROXY_HEALTH_PORT) || 7828;
|
|
1045
|
-
const start = Date.now();
|
|
1046
|
-
const timeoutMs = 15000;
|
|
1047
|
-
let ready = false;
|
|
1048
|
-
while (Date.now() - start < timeoutMs) {
|
|
1049
|
-
try {
|
|
1050
|
-
const res = await fetch(`http://localhost:${healthPort}/healthz`, {
|
|
1051
|
-
signal: AbortSignal.timeout(2000),
|
|
1052
|
-
});
|
|
1053
|
-
if (res.ok) {
|
|
1054
|
-
ready = true;
|
|
1055
|
-
break;
|
|
1056
|
-
}
|
|
1057
|
-
} catch {
|
|
1058
|
-
// Not ready yet
|
|
1059
|
-
}
|
|
1060
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
if (!ready) {
|
|
1064
|
-
console.warn(
|
|
1065
|
-
" ⚠️ Outbound proxy started but health check did not respond within 15s",
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
console.log("✅ Outbound proxy started\n");
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
925
|
/**
|
|
1073
|
-
* Stop any locally-running daemon
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
926
|
+
* Stop any locally-running daemon and gateway processes and clean up
|
|
927
|
+
* PID/socket files. Called when hatch fails partway through so we don't
|
|
928
|
+
* leave orphaned processes with no lock file entry.
|
|
1076
929
|
*/
|
|
1077
930
|
export async function stopLocalProcesses(): Promise<void> {
|
|
1078
931
|
const vellumDir = join(homedir(), ".vellum");
|
|
@@ -1083,6 +936,4 @@ export async function stopLocalProcesses(): Promise<void> {
|
|
|
1083
936
|
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
1084
937
|
await stopProcessByPidFile(gatewayPidFile, "gateway");
|
|
1085
938
|
|
|
1086
|
-
const outboundProxyPidFile = join(vellumDir, "outbound-proxy.pid");
|
|
1087
|
-
await stopProcessByPidFile(outboundProxyPidFile, "outbound-proxy");
|
|
1088
939
|
}
|