@vellumai/cli 0.3.3 → 0.3.5
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 +3 -2
- package/src/commands/client.ts +16 -1
- package/src/commands/email.ts +88 -0
- package/src/commands/hatch.ts +13 -1
- package/src/commands/ps.ts +46 -40
- package/src/commands/ssh.ts +111 -0
- package/src/email/vellum.ts +103 -0
- package/src/index.ts +37 -11
- package/src/lib/assistant-config.ts +18 -15
- package/src/lib/local.ts +50 -1
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "CLI tools for vellum-assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.ts",
|
|
8
8
|
"./package.json": "./package.json",
|
|
9
9
|
"./src/components/DefaultMainScreen": "./src/components/DefaultMainScreen.tsx",
|
|
10
|
-
"./src/lib/constants": "./src/lib/constants.ts"
|
|
10
|
+
"./src/lib/constants": "./src/lib/constants.ts",
|
|
11
|
+
"./src/commands/*": "./src/commands/*.ts"
|
|
11
12
|
},
|
|
12
13
|
"bin": {
|
|
13
14
|
"vellum-cli": "./src/index.ts"
|
package/src/commands/client.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
1
4
|
import { ANSI, renderChatApp } from "../components/DefaultMainScreen";
|
|
2
5
|
import { findAssistantByName, loadLatestAssistant } from "../lib/assistant-config";
|
|
3
6
|
import { GATEWAY_PORT, type Species } from "../lib/constants";
|
|
@@ -42,7 +45,19 @@ function parseArgs(): ParsedArgs {
|
|
|
42
45
|
|
|
43
46
|
let runtimeUrl = process.env.RUNTIME_URL || entry?.runtimeUrl || FALLBACK_RUNTIME_URL;
|
|
44
47
|
let assistantId = process.env.ASSISTANT_ID || entry?.assistantId || FALLBACK_ASSISTANT_ID;
|
|
45
|
-
|
|
48
|
+
let bearerToken = process.env.RUNTIME_PROXY_BEARER_TOKEN || entry?.bearerToken || undefined;
|
|
49
|
+
|
|
50
|
+
// For local assistants, read the daemon's http-token file as a fallback
|
|
51
|
+
// when the lockfile doesn't include a bearer token.
|
|
52
|
+
if (!bearerToken && entry?.cloud === "local") {
|
|
53
|
+
const tokenDir = entry.baseDataDir ?? join(process.env.HOME ?? "", ".vellum");
|
|
54
|
+
try {
|
|
55
|
+
const token = readFileSync(join(tokenDir, "http-token"), "utf-8").trim();
|
|
56
|
+
if (token) bearerToken = token;
|
|
57
|
+
} catch {
|
|
58
|
+
// Token file may not exist
|
|
59
|
+
}
|
|
60
|
+
}
|
|
46
61
|
const species: Species = (entry?.species as Species) ?? "vellum";
|
|
47
62
|
|
|
48
63
|
for (let i = 0; i < flagArgs.length; i++) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: `vellum email`
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - `vellum email status` — show current email configuration
|
|
6
|
+
* - `vellum email create <username>` — provision a new email inbox
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { VellumEmailClient } from "../email/vellum.js";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
function output(data: unknown): void {
|
|
16
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function exitError(message: string): void {
|
|
20
|
+
output({ ok: false, error: message });
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Usage
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
function printUsage(): void {
|
|
29
|
+
console.log(`Usage: vellum email <subcommand> [options]
|
|
30
|
+
|
|
31
|
+
Subcommands:
|
|
32
|
+
status Show email status (address, inboxes, callback URL)
|
|
33
|
+
create <username> Create a new email inbox for the given username
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
--help, -h Show this help message
|
|
37
|
+
`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Entry point
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export async function email(): Promise<void> {
|
|
45
|
+
const args = process.argv.slice(3); // everything after "email"
|
|
46
|
+
|
|
47
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
48
|
+
printUsage();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const subcommand = args[0];
|
|
53
|
+
|
|
54
|
+
switch (subcommand) {
|
|
55
|
+
case "status": {
|
|
56
|
+
try {
|
|
57
|
+
const client = new VellumEmailClient();
|
|
58
|
+
const status = await client.status();
|
|
59
|
+
output({
|
|
60
|
+
ok: true,
|
|
61
|
+
provider: status.provider,
|
|
62
|
+
inboxes: status.inboxes,
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
exitError(err instanceof Error ? err.message : String(err));
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "create": {
|
|
70
|
+
const username = args[1];
|
|
71
|
+
if (!username) {
|
|
72
|
+
exitError("Usage: vellum email create <username>");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const client = new VellumEmailClient();
|
|
77
|
+
const inbox = await client.createInbox(username);
|
|
78
|
+
output({ ok: true, inbox });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
exitError(err instanceof Error ? err.message : String(err));
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
default:
|
|
85
|
+
exitError(`Unknown email subcommand: ${subcommand}`);
|
|
86
|
+
printUsage();
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/commands/hatch.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "crypto";
|
|
2
|
-
import { existsSync, unlinkSync, writeFileSync } from "fs";
|
|
2
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
3
3
|
import { homedir, tmpdir, userInfo } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
|
|
@@ -549,10 +549,22 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
|
|
|
549
549
|
}
|
|
550
550
|
|
|
551
551
|
const baseDataDir = join(process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? userInfo().homedir), ".vellum");
|
|
552
|
+
|
|
553
|
+
// Read the bearer token written by the daemon so the client can authenticate
|
|
554
|
+
// with the gateway (which requires auth by default).
|
|
555
|
+
let bearerToken: string | undefined;
|
|
556
|
+
try {
|
|
557
|
+
const token = readFileSync(join(baseDataDir, "http-token"), "utf-8").trim();
|
|
558
|
+
if (token) bearerToken = token;
|
|
559
|
+
} catch {
|
|
560
|
+
// Token file may not exist if daemon started without HTTP server
|
|
561
|
+
}
|
|
562
|
+
|
|
552
563
|
const localEntry: AssistantEntry = {
|
|
553
564
|
assistantId: instanceName,
|
|
554
565
|
runtimeUrl,
|
|
555
566
|
baseDataDir,
|
|
567
|
+
bearerToken,
|
|
556
568
|
cloud: "local",
|
|
557
569
|
species,
|
|
558
570
|
hatchedAt: new Date().toISOString(),
|
package/src/commands/ps.ts
CHANGED
|
@@ -145,53 +145,59 @@ async function getRemoteProcessesCustom(
|
|
|
145
145
|
]);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
function
|
|
148
|
+
function checkPidFile(pidFile: string): { status: string; pid: string | null } {
|
|
149
|
+
if (!existsSync(pidFile)) {
|
|
150
|
+
return { status: "not running", pid: null };
|
|
151
|
+
}
|
|
152
|
+
const pid = readFileSync(pidFile, "utf-8").trim();
|
|
153
|
+
try {
|
|
154
|
+
process.kill(parseInt(pid, 10), 0);
|
|
155
|
+
return { status: "running", pid };
|
|
156
|
+
} catch {
|
|
157
|
+
return { status: "not running", pid };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function getLocalProcesses(entry: AssistantEntry): Promise<TableRow[]> {
|
|
162
|
+
const vellumDir = entry.baseDataDir ?? join(homedir(), ".vellum");
|
|
149
163
|
const rows: TableRow[] = [];
|
|
150
|
-
const vellumDir = join(homedir(), ".vellum");
|
|
151
164
|
|
|
152
165
|
// Check daemon PID
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
} catch {
|
|
160
|
-
status = "not running";
|
|
161
|
-
}
|
|
162
|
-
rows.push({ name: "daemon", status: withStatusEmoji(status), info: `PID ${pid}` });
|
|
163
|
-
} else {
|
|
164
|
-
rows.push({ name: "daemon", status: withStatusEmoji("not running"), info: "no PID file" });
|
|
165
|
-
}
|
|
166
|
+
const daemon = checkPidFile(join(vellumDir, "vellum.pid"));
|
|
167
|
+
rows.push({
|
|
168
|
+
name: "daemon",
|
|
169
|
+
status: withStatusEmoji(daemon.status),
|
|
170
|
+
info: daemon.pid ? `PID ${daemon.pid}` : "no PID file",
|
|
171
|
+
});
|
|
166
172
|
|
|
167
173
|
// Check qdrant PID
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
} catch {
|
|
175
|
-
status = "not running";
|
|
176
|
-
}
|
|
177
|
-
rows.push({ name: "qdrant", status: withStatusEmoji(status), info: `PID ${pid} | port 6333` });
|
|
178
|
-
} else {
|
|
179
|
-
rows.push({ name: "qdrant", status: withStatusEmoji("not running"), info: "no PID file" });
|
|
180
|
-
}
|
|
174
|
+
const qdrant = checkPidFile(join(vellumDir, "workspace", "data", "qdrant", "qdrant.pid"));
|
|
175
|
+
rows.push({
|
|
176
|
+
name: "qdrant",
|
|
177
|
+
status: withStatusEmoji(qdrant.status),
|
|
178
|
+
info: qdrant.pid ? `PID ${qdrant.pid} | port 6333` : "no PID file",
|
|
179
|
+
});
|
|
181
180
|
|
|
182
181
|
// Check gateway PID
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
182
|
+
const gateway = checkPidFile(join(vellumDir, "gateway.pid"));
|
|
183
|
+
rows.push({
|
|
184
|
+
name: "gateway",
|
|
185
|
+
status: withStatusEmoji(gateway.status),
|
|
186
|
+
info: gateway.pid ? `PID ${gateway.pid} | port 7830` : "no PID file",
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// If no PID files found, fall back to health check
|
|
190
|
+
const allMissingPid = !daemon.pid && !qdrant.pid && !gateway.pid;
|
|
191
|
+
if (allMissingPid) {
|
|
192
|
+
const health = await checkHealth(entry.runtimeUrl);
|
|
193
|
+
if (health.status === "healthy" || health.status === "ok") {
|
|
194
|
+
rows.length = 0;
|
|
195
|
+
rows.push({
|
|
196
|
+
name: "daemon",
|
|
197
|
+
status: withStatusEmoji("running"),
|
|
198
|
+
info: "no PID file (detected via health check)",
|
|
199
|
+
});
|
|
191
200
|
}
|
|
192
|
-
rows.push({ name: "gateway", status: withStatusEmoji(status), info: `PID ${pid} | port 7830` });
|
|
193
|
-
} else {
|
|
194
|
-
rows.push({ name: "gateway", status: withStatusEmoji("not running"), info: "no PID file" });
|
|
195
201
|
}
|
|
196
202
|
|
|
197
203
|
return rows;
|
|
@@ -203,7 +209,7 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
203
209
|
console.log(`Processes for ${entry.assistantId} (${cloud}):\n`);
|
|
204
210
|
|
|
205
211
|
if (cloud === "local") {
|
|
206
|
-
const rows = getLocalProcesses();
|
|
212
|
+
const rows = await getLocalProcesses(entry);
|
|
207
213
|
printTable(rows);
|
|
208
214
|
return;
|
|
209
215
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
|
|
3
|
+
import { findAssistantByName, loadLatestAssistant } from "../lib/assistant-config";
|
|
4
|
+
import type { AssistantEntry } from "../lib/assistant-config";
|
|
5
|
+
|
|
6
|
+
const SSH_OPTS = [
|
|
7
|
+
"-o", "StrictHostKeyChecking=no",
|
|
8
|
+
"-o", "UserKnownHostsFile=/dev/null",
|
|
9
|
+
"-o", "ConnectTimeout=10",
|
|
10
|
+
"-o", "LogLevel=ERROR",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function resolveCloud(entry: AssistantEntry): string {
|
|
14
|
+
if (entry.cloud) {
|
|
15
|
+
return entry.cloud;
|
|
16
|
+
}
|
|
17
|
+
if (entry.project) {
|
|
18
|
+
return "gcp";
|
|
19
|
+
}
|
|
20
|
+
if (entry.sshUser) {
|
|
21
|
+
return "custom";
|
|
22
|
+
}
|
|
23
|
+
return "local";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractHostFromUrl(url: string): string {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = new URL(url);
|
|
29
|
+
return parsed.hostname;
|
|
30
|
+
} catch {
|
|
31
|
+
return url.replace(/^https?:\/\//, "").split(":")[0];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function ssh(): Promise<void> {
|
|
36
|
+
const name = process.argv[3];
|
|
37
|
+
const entry = name ? findAssistantByName(name) : loadLatestAssistant();
|
|
38
|
+
|
|
39
|
+
if (!entry) {
|
|
40
|
+
if (name) {
|
|
41
|
+
console.error(`No assistant instance found with name '${name}'.`);
|
|
42
|
+
} else {
|
|
43
|
+
console.error("No assistant instance found. Run `vellum-cli hatch` first.");
|
|
44
|
+
}
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const cloud = resolveCloud(entry);
|
|
49
|
+
|
|
50
|
+
if (cloud === "local") {
|
|
51
|
+
console.error(
|
|
52
|
+
"Cannot SSH into a local assistant. Local assistants run directly on this machine.\n" +
|
|
53
|
+
`Use 'vellum-cli ps ${entry.assistantId}' to check its processes instead.`,
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (cloud === "aws") {
|
|
59
|
+
console.error("SSH to AWS instances is not yet supported.");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let child;
|
|
64
|
+
|
|
65
|
+
if (cloud === "gcp") {
|
|
66
|
+
const project = entry.project;
|
|
67
|
+
const zone = entry.zone;
|
|
68
|
+
if (!project || !zone) {
|
|
69
|
+
console.error("Error: GCP project and zone not found in assistant config.");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sshTarget = entry.sshUser
|
|
74
|
+
? `${entry.sshUser}@${entry.assistantId}`
|
|
75
|
+
: entry.assistantId;
|
|
76
|
+
|
|
77
|
+
console.log(`🔗 Connecting to ${entry.assistantId} via gcloud...\n`);
|
|
78
|
+
|
|
79
|
+
child = spawn(
|
|
80
|
+
"gcloud",
|
|
81
|
+
["compute", "ssh", sshTarget, `--project=${project}`, `--zone=${zone}`],
|
|
82
|
+
{ stdio: "inherit" },
|
|
83
|
+
);
|
|
84
|
+
} else if (cloud === "custom") {
|
|
85
|
+
const host = extractHostFromUrl(entry.runtimeUrl);
|
|
86
|
+
const sshUser = entry.sshUser ?? "root";
|
|
87
|
+
const sshTarget = `${sshUser}@${host}`;
|
|
88
|
+
|
|
89
|
+
console.log(`🔗 Connecting to ${entry.assistantId} via ssh...\n`);
|
|
90
|
+
|
|
91
|
+
child = spawn(
|
|
92
|
+
"ssh",
|
|
93
|
+
[...SSH_OPTS, sshTarget],
|
|
94
|
+
{ stdio: "inherit" },
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
console.error(`Error: Unknown cloud type '${cloud}'.`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await new Promise<void>((resolve, reject) => {
|
|
102
|
+
child.on("close", (code) => {
|
|
103
|
+
if (code === 0) {
|
|
104
|
+
resolve();
|
|
105
|
+
} else {
|
|
106
|
+
reject(new Error(`ssh exited with code ${code}`));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
child.on("error", reject);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vellum email API client — calls the Vellum platform email endpoints.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// The domain for the Vellum email API is still being finalized and may change.
|
|
6
|
+
const DEFAULT_VELLUM_API_URL = "https://api.vellum.ai";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Types
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export interface EmailInbox {
|
|
13
|
+
id: string;
|
|
14
|
+
address: string;
|
|
15
|
+
displayName?: string;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface EmailStatus {
|
|
20
|
+
provider: string;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
inboxes: EmailInbox[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// HTTP helper
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
async function vellumFetch(
|
|
30
|
+
apiKey: string,
|
|
31
|
+
baseUrl: string,
|
|
32
|
+
path: string,
|
|
33
|
+
opts: { method?: string; body?: unknown } = {},
|
|
34
|
+
): Promise<unknown> {
|
|
35
|
+
const url = `${baseUrl}${path}`;
|
|
36
|
+
const response = await fetch(url, {
|
|
37
|
+
method: opts.method ?? "GET",
|
|
38
|
+
headers: {
|
|
39
|
+
Authorization: `Bearer ${apiKey}`,
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
},
|
|
42
|
+
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const text = await response.text().catch(() => "");
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Vellum email API error: ${response.status} ${response.statusText}${text ? ` — ${text}` : ""}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
53
|
+
if (contentType.includes("application/json")) {
|
|
54
|
+
return response.json();
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Client
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
export class VellumEmailClient {
|
|
64
|
+
private apiKey: string;
|
|
65
|
+
private baseUrl: string;
|
|
66
|
+
|
|
67
|
+
constructor(apiKey?: string, baseUrl?: string) {
|
|
68
|
+
const resolvedKey = apiKey ?? process.env.VELLUM_API_KEY;
|
|
69
|
+
if (!resolvedKey) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"No Vellum API key configured. Set the VELLUM_API_KEY environment variable.",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
this.apiKey = resolvedKey;
|
|
75
|
+
this.baseUrl =
|
|
76
|
+
baseUrl ?? process.env.VELLUM_API_URL ?? DEFAULT_VELLUM_API_URL;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** List existing email addresses and check connectivity. */
|
|
80
|
+
async status(): Promise<EmailStatus> {
|
|
81
|
+
const result = await vellumFetch(
|
|
82
|
+
this.apiKey,
|
|
83
|
+
this.baseUrl,
|
|
84
|
+
"/v1/email-addresses",
|
|
85
|
+
);
|
|
86
|
+
const inboxes = (result as { inboxes: EmailInbox[] }).inboxes;
|
|
87
|
+
return { provider: "vellum", ok: true, inboxes };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Provision a new email address for the given username. */
|
|
91
|
+
async createInbox(username: string): Promise<EmailInbox> {
|
|
92
|
+
const result = await vellumFetch(
|
|
93
|
+
this.apiKey,
|
|
94
|
+
this.baseUrl,
|
|
95
|
+
"/v1/email-addresses",
|
|
96
|
+
{
|
|
97
|
+
method: "POST",
|
|
98
|
+
body: { username },
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
return result as EmailInbox;
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,26 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
6
8
|
import { client } from "./commands/client";
|
|
9
|
+
import { email } from "./commands/email";
|
|
7
10
|
import { hatch } from "./commands/hatch";
|
|
8
11
|
import { ps } from "./commands/ps";
|
|
9
12
|
import { retire } from "./commands/retire";
|
|
10
13
|
import { sleep } from "./commands/sleep";
|
|
14
|
+
import { ssh } from "./commands/ssh";
|
|
11
15
|
import { wake } from "./commands/wake";
|
|
12
16
|
|
|
13
17
|
const commands = {
|
|
14
18
|
client,
|
|
19
|
+
email,
|
|
15
20
|
hatch,
|
|
16
21
|
ps,
|
|
17
22
|
retire,
|
|
18
23
|
sleep,
|
|
24
|
+
ssh,
|
|
19
25
|
wake,
|
|
20
26
|
} as const;
|
|
21
27
|
|
|
22
28
|
type CommandName = keyof typeof commands;
|
|
23
29
|
|
|
30
|
+
function resolveAssistantEntry(): string | undefined {
|
|
31
|
+
// When installed globally, resolve from node_modules
|
|
32
|
+
try {
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
const assistantPkgPath = require.resolve(
|
|
35
|
+
"@vellumai/assistant/package.json"
|
|
36
|
+
);
|
|
37
|
+
return join(dirname(assistantPkgPath), "src", "index.ts");
|
|
38
|
+
} catch {
|
|
39
|
+
// For local development, resolve from sibling directory
|
|
40
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
const localPath = join(
|
|
42
|
+
__dirname,
|
|
43
|
+
"..",
|
|
44
|
+
"..",
|
|
45
|
+
"assistant",
|
|
46
|
+
"src",
|
|
47
|
+
"index.ts"
|
|
48
|
+
);
|
|
49
|
+
if (existsSync(localPath)) {
|
|
50
|
+
return localPath;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
24
56
|
async function main() {
|
|
25
57
|
const args = process.argv.slice(2);
|
|
26
58
|
const commandName = args[0];
|
|
@@ -30,10 +62,12 @@ async function main() {
|
|
|
30
62
|
console.log("");
|
|
31
63
|
console.log("Commands:");
|
|
32
64
|
console.log(" client Connect to a hatched assistant");
|
|
65
|
+
console.log(" email Email operations (status, create inbox)");
|
|
33
66
|
console.log(" hatch Create a new assistant instance");
|
|
34
67
|
console.log(" ps List assistants (or processes for a specific assistant)");
|
|
35
68
|
console.log(" retire Delete an assistant instance");
|
|
36
69
|
console.log(" sleep Stop the daemon process");
|
|
70
|
+
console.log(" ssh SSH into a remote assistant instance");
|
|
37
71
|
console.log(" wake Start the daemon and gateway");
|
|
38
72
|
process.exit(0);
|
|
39
73
|
}
|
|
@@ -41,23 +75,15 @@ async function main() {
|
|
|
41
75
|
const command = commands[commandName as CommandName];
|
|
42
76
|
|
|
43
77
|
if (!command) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const assistantPkgPath = require.resolve(
|
|
47
|
-
"@vellumai/assistant/package.json"
|
|
48
|
-
);
|
|
49
|
-
const assistantEntry = join(
|
|
50
|
-
dirname(assistantPkgPath),
|
|
51
|
-
"src",
|
|
52
|
-
"index.ts"
|
|
53
|
-
);
|
|
78
|
+
const assistantEntry = resolveAssistantEntry();
|
|
79
|
+
if (assistantEntry) {
|
|
54
80
|
const child = spawn("bun", ["run", assistantEntry, ...args], {
|
|
55
81
|
stdio: "inherit",
|
|
56
82
|
});
|
|
57
83
|
child.on("exit", (code) => {
|
|
58
84
|
process.exit(code ?? 1);
|
|
59
85
|
});
|
|
60
|
-
}
|
|
86
|
+
} else {
|
|
61
87
|
console.error(`Unknown command: ${commandName}`);
|
|
62
88
|
console.error(
|
|
63
89
|
"Install the full stack with: bun install -g vellum"
|
|
@@ -22,30 +22,33 @@ interface LockfileData {
|
|
|
22
22
|
[key: string]: unknown;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function
|
|
26
|
-
return
|
|
25
|
+
function getBaseDir(): string {
|
|
26
|
+
return process.env.BASE_DATA_DIR?.trim() || homedir();
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function readLockfile(): LockfileData {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
const base = getBaseDir();
|
|
31
|
+
const candidates = [
|
|
32
|
+
join(base, ".vellum.lock.json"),
|
|
33
|
+
join(base, ".vellum.lockfile.json"),
|
|
34
|
+
];
|
|
35
|
+
for (const lockfilePath of candidates) {
|
|
36
|
+
if (!existsSync(lockfilePath)) continue;
|
|
37
|
+
try {
|
|
38
|
+
const raw = readFileSync(lockfilePath, "utf-8");
|
|
39
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
40
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
41
|
+
return parsed as LockfileData;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Malformed lockfile; try next
|
|
40
45
|
}
|
|
41
|
-
} catch {
|
|
42
|
-
// Malformed lockfile; return empty
|
|
43
46
|
}
|
|
44
47
|
return {};
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
function writeLockfile(data: LockfileData): void {
|
|
48
|
-
const lockfilePath =
|
|
51
|
+
const lockfilePath = join(getBaseDir(), ".vellum.lock.json");
|
|
49
52
|
writeFileSync(lockfilePath, JSON.stringify(data, null, 2) + "\n");
|
|
50
53
|
}
|
|
51
54
|
|
package/src/lib/local.ts
CHANGED
|
@@ -302,13 +302,62 @@ export async function startGateway(): Promise<string> {
|
|
|
302
302
|
const assistants = loadAllAssistants();
|
|
303
303
|
const isSingleAssistant = assistants.length === 1;
|
|
304
304
|
|
|
305
|
+
// Read the bearer token so the gateway can authenticate proxied requests
|
|
306
|
+
// (e.g. from paired iOS devices). Respect VELLUM_HTTP_TOKEN_PATH and
|
|
307
|
+
// BASE_DATA_DIR for consistency with gateway/config.ts and the daemon.
|
|
308
|
+
const httpTokenPath = process.env.VELLUM_HTTP_TOKEN_PATH
|
|
309
|
+
?? join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum", "http-token");
|
|
310
|
+
let runtimeProxyBearerToken: string | undefined;
|
|
311
|
+
try {
|
|
312
|
+
const tok = readFileSync(httpTokenPath, "utf-8").trim();
|
|
313
|
+
if (tok) runtimeProxyBearerToken = tok;
|
|
314
|
+
} catch {
|
|
315
|
+
// Token file doesn't exist yet — daemon hasn't written it.
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// If no token is available (first startup — daemon hasn't written it yet),
|
|
319
|
+
// poll for the file to appear. The daemon writes the token shortly after
|
|
320
|
+
// startup, so a short wait avoids starting the gateway without auth
|
|
321
|
+
// (which would leave it permanently unauthenticated since the gateway
|
|
322
|
+
// config is loaded once at startup and never reloads).
|
|
323
|
+
if (!runtimeProxyBearerToken) {
|
|
324
|
+
console.log(" Waiting for bearer token file...");
|
|
325
|
+
const maxWait = 10000;
|
|
326
|
+
const pollInterval = 500;
|
|
327
|
+
const start = Date.now();
|
|
328
|
+
while (Date.now() - start < maxWait) {
|
|
329
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
330
|
+
try {
|
|
331
|
+
const tok = readFileSync(httpTokenPath, "utf-8").trim();
|
|
332
|
+
if (tok) {
|
|
333
|
+
runtimeProxyBearerToken = tok;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
// File still doesn't exist, keep polling.
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// If the token still isn't available after polling, fall back to starting
|
|
343
|
+
// the gateway without auth so it doesn't block forever. This is a degraded
|
|
344
|
+
// mode — the proxy will be broken because the runtime expects a token.
|
|
345
|
+
const proxyRequireAuth = runtimeProxyBearerToken ? "true" : "false";
|
|
346
|
+
if (!runtimeProxyBearerToken) {
|
|
347
|
+
console.log(" ⚠️ Bearer token not found after 10s — gateway proxy auth disabled");
|
|
348
|
+
}
|
|
349
|
+
|
|
305
350
|
const gatewayEnv: Record<string, string> = {
|
|
306
351
|
...process.env as Record<string, string>,
|
|
307
352
|
GATEWAY_RUNTIME_PROXY_ENABLED: "true",
|
|
308
|
-
GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH:
|
|
353
|
+
GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: proxyRequireAuth,
|
|
309
354
|
RUNTIME_HTTP_PORT: process.env.RUNTIME_HTTP_PORT || "7821",
|
|
310
355
|
};
|
|
311
356
|
|
|
357
|
+
if (runtimeProxyBearerToken) {
|
|
358
|
+
gatewayEnv.RUNTIME_PROXY_BEARER_TOKEN = runtimeProxyBearerToken;
|
|
359
|
+
}
|
|
360
|
+
|
|
312
361
|
if (process.env.GATEWAY_UNMAPPED_POLICY) {
|
|
313
362
|
gatewayEnv.GATEWAY_UNMAPPED_POLICY = process.env.GATEWAY_UNMAPPED_POLICY;
|
|
314
363
|
} else if (isSingleAssistant) {
|