@vellumai/cli 0.4.34 → 0.4.36

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/AGENTS.md ADDED
@@ -0,0 +1,47 @@
1
+ # CLI Package — Agent Instructions
2
+
3
+ ## Purpose
4
+
5
+ The `cli/` package (`@vellumai/cli`) manages the **lifecycle of Vellum assistant instances** — creating, starting, stopping, connecting to, and deleting them. Commands here operate on or across instances and typically require specifying which assistant to target.
6
+
7
+ This contrasts with `assistant/src/cli/`, where commands are scoped to a **single running assistant** and operate on its local state (config, memory, contacts, etc.).
8
+
9
+ ## When a command belongs here vs `assistant/src/cli/`
10
+
11
+ | `cli/` (this package) | `assistant/src/cli/` |
12
+ | ----------------------------------------------- | --------------------------------------------------- |
13
+ | Operates on or across assistant instances | Operates within a single assistant's workspace |
14
+ | Manages lifecycle (create, start, stop, delete) | Manages instance-local state (config, memory, etc.) |
15
+ | Requires specifying which assistant to target | Implicitly scoped to the running assistant |
16
+ | Works without an assistant process running | May require or start the daemon |
17
+
18
+ Examples: `hatch`, `wake`, `sleep`, `retire`, `ps`, `ssh` belong here. `config`, `contacts`, `memory` belong in `assistant/src/cli/`.
19
+
20
+ ## Assistant targeting convention
21
+
22
+ Commands that act on a specific assistant should accept an assistant name or ID as an argument. When none is specified, default to the most recently created local assistant. Use `loadAllAssistants()` and `findAssistantByName()` from `lib/assistant-config` for resolution.
23
+
24
+ ## Conventions
25
+
26
+ - Commands are standalone exported functions in `src/commands/`.
27
+ - Each command manually parses `process.argv.slice(3)` (no framework — keep it lightweight).
28
+ - Register new commands in the `commands` object in `src/index.ts` and add a help line.
29
+ - User-facing output uses `console.log`/`console.error` directly (no shared logger).
30
+
31
+ ## Help Text Standards
32
+
33
+ Every command must have high-quality `--help` output optimized for AI/LLM
34
+ consumption. Help text is a primary interface — both humans and AI agents read
35
+ it to understand what a command does and how to use it.
36
+
37
+ ### Requirements
38
+
39
+ 1. **Each command**: Include a concise one-liner description in the help output,
40
+ followed by an explanation of arguments/options with their formats and
41
+ constraints.
42
+
43
+ 2. **Include examples**: Show 2-3 concrete invocations with realistic values.
44
+
45
+ 3. **Write for machines**: Be precise about formats, constraints, and side effects.
46
+ AI agents parse help text to decide which command to run and how. Avoid vague
47
+ language — say exactly what the command does and where state is stored.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.4.34",
3
+ "version": "0.4.36",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
package/src/index.ts CHANGED
@@ -7,11 +7,7 @@ import { spawn } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
9
9
  import cliPkg from "../package.json";
10
- import { autonomy } from "./commands/autonomy";
11
10
  import { client } from "./commands/client";
12
- import { config } from "./commands/config";
13
- import { contacts } from "./commands/contacts";
14
- import { email } from "./commands/email";
15
11
  import { hatch } from "./commands/hatch";
16
12
  import { login, logout, whoami } from "./commands/login";
17
13
  import { pair } from "./commands/pair";
@@ -25,11 +21,7 @@ import { tunnel } from "./commands/tunnel";
25
21
  import { wake } from "./commands/wake";
26
22
 
27
23
  const commands = {
28
- autonomy,
29
24
  client,
30
- config,
31
- contacts,
32
- email,
33
25
  hatch,
34
26
  login,
35
27
  logout,
@@ -88,8 +80,8 @@ async function main() {
88
80
  console.log(" autonomy View and configure autonomy tiers");
89
81
  console.log(" client Connect to a hatched assistant");
90
82
  console.log(" config Manage configuration");
91
- console.log(" contacts Manage the contact graph");
92
- console.log(" email Email operations (status, create inbox)");
83
+ console.log(" contacts Manage assistant contacts");
84
+ console.log(" email Email operations (provider-agnostic)");
93
85
  console.log(" hatch Create a new assistant instance");
94
86
  console.log(" login Log in to the Vellum platform");
95
87
  console.log(" logout Log out of the Vellum platform");
package/src/lib/local.ts CHANGED
@@ -701,6 +701,7 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
701
701
  "VELLUM_DAEMON_TCP_PORT",
702
702
  "VELLUM_DAEMON_TCP_HOST",
703
703
  "VELLUM_DAEMON_SOCKET",
704
+ "VELLUM_KEYCHAIN_BROKER_SOCKET",
704
705
  "VELLUM_DEBUG",
705
706
  "SENTRY_DSN",
706
707
  "TMPDIR",
package/src/lib/ngrok.ts CHANGED
@@ -1,8 +1,31 @@
1
1
  import { execFileSync, spawn, type ChildProcess } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
2
5
 
3
- import { loadRawConfig, saveRawConfig } from "./config";
4
6
  import { GATEWAY_PORT } from "./constants";
5
7
 
8
+ function getConfigPath(): string {
9
+ const root = join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum");
10
+ return join(root, "workspace", "config.json");
11
+ }
12
+
13
+ function loadRawConfig(): Record<string, unknown> {
14
+ const configPath = getConfigPath();
15
+ if (!existsSync(configPath)) return {};
16
+ return JSON.parse(readFileSync(configPath, "utf-8")) as Record<
17
+ string,
18
+ unknown
19
+ >;
20
+ }
21
+
22
+ function saveRawConfig(config: Record<string, unknown>): void {
23
+ const configPath = getConfigPath();
24
+ const dir = dirname(configPath);
25
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
26
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
27
+ }
28
+
6
29
  const NGROK_API_URL = "http://127.0.0.1:4040/api/tunnels";
7
30
  const NGROK_POLL_INTERVAL_MS = 500;
8
31
  const NGROK_POLL_TIMEOUT_MS = 15_000;
@@ -1,320 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
-
5
- // ---------------------------------------------------------------------------
6
- // Types & constants (ported from assistant/src/autonomy/types.ts)
7
- // ---------------------------------------------------------------------------
8
-
9
- type AutonomyTier = "auto" | "draft" | "notify";
10
-
11
- const AUTONOMY_TIERS: readonly AutonomyTier[] = ["auto", "draft", "notify"];
12
-
13
- interface AutonomyConfig {
14
- defaultTier: AutonomyTier;
15
- channelDefaults: Record<string, AutonomyTier>;
16
- categoryOverrides: Record<string, AutonomyTier>;
17
- contactOverrides: Record<string, AutonomyTier>;
18
- }
19
-
20
- const DEFAULT_AUTONOMY_CONFIG: AutonomyConfig = {
21
- defaultTier: "notify",
22
- channelDefaults: {},
23
- categoryOverrides: {},
24
- contactOverrides: {},
25
- };
26
-
27
- // ---------------------------------------------------------------------------
28
- // Config persistence (ported from assistant/src/autonomy/autonomy-store.ts)
29
- // ---------------------------------------------------------------------------
30
-
31
- function getConfigPath(): string {
32
- const root = join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum");
33
- return join(root, "workspace", "autonomy.json");
34
- }
35
-
36
- function isValidTier(value: unknown): value is AutonomyTier {
37
- return (
38
- typeof value === "string" && AUTONOMY_TIERS.includes(value as AutonomyTier)
39
- );
40
- }
41
-
42
- function validateTierRecord(raw: unknown): Record<string, AutonomyTier> {
43
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
44
- const result: Record<string, AutonomyTier> = {};
45
- for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
46
- if (isValidTier(value)) {
47
- result[key] = value;
48
- }
49
- }
50
- return result;
51
- }
52
-
53
- function validateConfig(raw: unknown): AutonomyConfig {
54
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
55
- return structuredClone(DEFAULT_AUTONOMY_CONFIG);
56
- }
57
- const obj = raw as Record<string, unknown>;
58
- return {
59
- defaultTier: isValidTier(obj.defaultTier)
60
- ? obj.defaultTier
61
- : DEFAULT_AUTONOMY_CONFIG.defaultTier,
62
- channelDefaults: validateTierRecord(obj.channelDefaults),
63
- categoryOverrides: validateTierRecord(obj.categoryOverrides),
64
- contactOverrides: validateTierRecord(obj.contactOverrides),
65
- };
66
- }
67
-
68
- function loadConfig(): AutonomyConfig {
69
- const configPath = getConfigPath();
70
- if (!existsSync(configPath)) {
71
- return structuredClone(DEFAULT_AUTONOMY_CONFIG);
72
- }
73
- try {
74
- const raw = readFileSync(configPath, "utf-8");
75
- return validateConfig(JSON.parse(raw));
76
- } catch {
77
- console.error("Warning: failed to parse autonomy config; using defaults");
78
- return structuredClone(DEFAULT_AUTONOMY_CONFIG);
79
- }
80
- }
81
-
82
- function saveConfig(config: AutonomyConfig): void {
83
- const configPath = getConfigPath();
84
- mkdirSync(dirname(configPath), { recursive: true });
85
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
86
- }
87
-
88
- function applyUpdate(updates: Partial<AutonomyConfig>): AutonomyConfig {
89
- const current = loadConfig();
90
- if (updates.defaultTier !== undefined) {
91
- current.defaultTier = updates.defaultTier;
92
- }
93
- if (updates.channelDefaults !== undefined) {
94
- current.channelDefaults = {
95
- ...current.channelDefaults,
96
- ...updates.channelDefaults,
97
- };
98
- }
99
- if (updates.categoryOverrides !== undefined) {
100
- current.categoryOverrides = {
101
- ...current.categoryOverrides,
102
- ...updates.categoryOverrides,
103
- };
104
- }
105
- if (updates.contactOverrides !== undefined) {
106
- current.contactOverrides = {
107
- ...current.contactOverrides,
108
- ...updates.contactOverrides,
109
- };
110
- }
111
- saveConfig(current);
112
- return current;
113
- }
114
-
115
- // ---------------------------------------------------------------------------
116
- // Output helpers
117
- // ---------------------------------------------------------------------------
118
-
119
- function output(data: unknown, json: boolean): void {
120
- process.stdout.write(
121
- json ? JSON.stringify(data) + "\n" : JSON.stringify(data, null, 2) + "\n",
122
- );
123
- }
124
-
125
- function formatConfigForHuman(config: AutonomyConfig): string {
126
- const lines: string[] = [` Default tier: ${config.defaultTier}`];
127
-
128
- const channelEntries = Object.entries(config.channelDefaults);
129
- if (channelEntries.length > 0) {
130
- lines.push(" Channel defaults:");
131
- for (const [channel, tier] of channelEntries) {
132
- lines.push(` ${channel}: ${tier}`);
133
- }
134
- } else {
135
- lines.push(" Channel defaults: (none)");
136
- }
137
-
138
- const categoryEntries = Object.entries(config.categoryOverrides);
139
- if (categoryEntries.length > 0) {
140
- lines.push(" Category overrides:");
141
- for (const [category, tier] of categoryEntries) {
142
- lines.push(` ${category}: ${tier}`);
143
- }
144
- } else {
145
- lines.push(" Category overrides: (none)");
146
- }
147
-
148
- const contactEntries = Object.entries(config.contactOverrides);
149
- if (contactEntries.length > 0) {
150
- lines.push(" Contact overrides:");
151
- for (const [contactId, tier] of contactEntries) {
152
- lines.push(` ${contactId}: ${tier}`);
153
- }
154
- } else {
155
- lines.push(" Contact overrides: (none)");
156
- }
157
-
158
- return lines.join("\n");
159
- }
160
-
161
- // ---------------------------------------------------------------------------
162
- // Arg parsing helpers
163
- // ---------------------------------------------------------------------------
164
-
165
- function hasFlag(args: string[], flag: string): boolean {
166
- return args.includes(flag);
167
- }
168
-
169
- function getFlagValue(args: string[], flag: string): string | undefined {
170
- const idx = args.indexOf(flag);
171
- if (idx === -1 || idx + 1 >= args.length) return undefined;
172
- return args[idx + 1];
173
- }
174
-
175
- // ---------------------------------------------------------------------------
176
- // Usage
177
- // ---------------------------------------------------------------------------
178
-
179
- function printUsage(): void {
180
- console.log("Usage: vellum autonomy <subcommand> [options]");
181
- console.log("");
182
- console.log("Subcommands:");
183
- console.log(
184
- " get Show current autonomy configuration",
185
- );
186
- console.log(" set --default <tier> Set the global default tier");
187
- console.log(" set --channel <ch> --tier <t> Set tier for a channel");
188
- console.log(" set --category <cat> --tier <t> Set tier for a category");
189
- console.log(" set --contact <id> --tier <t> Set tier for a contact");
190
- console.log("");
191
- console.log("Options:");
192
- console.log(" --json Machine-readable JSON output");
193
- console.log("");
194
- console.log("Tiers: auto, draft, notify");
195
- }
196
-
197
- // ---------------------------------------------------------------------------
198
- // Command entry point
199
- // ---------------------------------------------------------------------------
200
-
201
- export function autonomy(): void {
202
- const args = process.argv.slice(3);
203
- const subcommand = args[0];
204
- const json = hasFlag(args, "--json");
205
-
206
- if (!subcommand || subcommand === "--help" || subcommand === "-h") {
207
- printUsage();
208
- return;
209
- }
210
-
211
- switch (subcommand) {
212
- case "get": {
213
- const config = loadConfig();
214
- if (json) {
215
- output({ ok: true, config }, true);
216
- } else {
217
- process.stdout.write("Autonomy configuration:\n\n");
218
- process.stdout.write(formatConfigForHuman(config) + "\n");
219
- }
220
- break;
221
- }
222
-
223
- case "set": {
224
- const defaultTier = getFlagValue(args, "--default");
225
- const channel = getFlagValue(args, "--channel");
226
- const category = getFlagValue(args, "--category");
227
- const contact = getFlagValue(args, "--contact");
228
- const tier = getFlagValue(args, "--tier");
229
-
230
- if (defaultTier) {
231
- if (!isValidTier(defaultTier)) {
232
- output(
233
- {
234
- ok: false,
235
- error: `Invalid tier "${defaultTier}". Must be one of: ${AUTONOMY_TIERS.join(", ")}`,
236
- },
237
- true,
238
- );
239
- process.exitCode = 1;
240
- return;
241
- }
242
- const config = applyUpdate({ defaultTier });
243
- if (json) {
244
- output({ ok: true, config }, true);
245
- } else {
246
- console.log(`Set global default tier to "${defaultTier}".`);
247
- }
248
- return;
249
- }
250
-
251
- if (!tier) {
252
- output(
253
- {
254
- ok: false,
255
- error: "Missing --tier. Use --tier <auto|draft|notify>.",
256
- },
257
- true,
258
- );
259
- process.exitCode = 1;
260
- return;
261
- }
262
- if (!isValidTier(tier)) {
263
- output(
264
- {
265
- ok: false,
266
- error: `Invalid tier "${tier}". Must be one of: ${AUTONOMY_TIERS.join(", ")}`,
267
- },
268
- true,
269
- );
270
- process.exitCode = 1;
271
- return;
272
- }
273
-
274
- if (channel) {
275
- const config = applyUpdate({ channelDefaults: { [channel]: tier } });
276
- if (json) {
277
- output({ ok: true, config }, true);
278
- } else {
279
- console.log(`Set channel "${channel}" default to "${tier}".`);
280
- }
281
- return;
282
- }
283
-
284
- if (category) {
285
- const config = applyUpdate({
286
- categoryOverrides: { [category]: tier },
287
- });
288
- if (json) {
289
- output({ ok: true, config }, true);
290
- } else {
291
- console.log(`Set category "${category}" override to "${tier}".`);
292
- }
293
- return;
294
- }
295
-
296
- if (contact) {
297
- const config = applyUpdate({ contactOverrides: { [contact]: tier } });
298
- if (json) {
299
- output({ ok: true, config }, true);
300
- } else {
301
- console.log(`Set contact "${contact}" override to "${tier}".`);
302
- }
303
- return;
304
- }
305
-
306
- console.error(
307
- "Specify one of: --default <tier>, --channel <channel> --tier <tier>, " +
308
- "--category <category> --tier <tier>, or --contact <contactId> --tier <tier>.",
309
- );
310
- process.exitCode = 1;
311
- break;
312
- }
313
-
314
- default: {
315
- console.error(`Unknown autonomy subcommand: ${subcommand}`);
316
- printUsage();
317
- process.exit(1);
318
- }
319
- }
320
- }
@@ -1,178 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
-
3
- import { syncConfigToLockfile } from "../lib/assistant-config";
4
- import {
5
- getAllowlistPath,
6
- getNestedValue,
7
- loadRawConfig,
8
- saveRawConfig,
9
- setNestedValue,
10
- } from "../lib/config";
11
-
12
- interface AllowlistConfig {
13
- values?: string[];
14
- prefixes?: string[];
15
- patterns?: string[];
16
- }
17
-
18
- interface AllowlistValidationError {
19
- index: number;
20
- pattern: string;
21
- message: string;
22
- }
23
-
24
- function validateAllowlist(
25
- allowlistConfig: AllowlistConfig,
26
- ): AllowlistValidationError[] {
27
- const errors: AllowlistValidationError[] = [];
28
- if (!allowlistConfig.patterns) return errors;
29
- if (!Array.isArray(allowlistConfig.patterns)) {
30
- errors.push({
31
- index: -1,
32
- pattern: String(allowlistConfig.patterns),
33
- message: '"patterns" must be an array',
34
- });
35
- return errors;
36
- }
37
-
38
- for (let i = 0; i < allowlistConfig.patterns.length; i++) {
39
- const p = allowlistConfig.patterns[i];
40
- if (typeof p !== "string") {
41
- errors.push({
42
- index: i,
43
- pattern: String(p),
44
- message: "Pattern is not a string",
45
- });
46
- continue;
47
- }
48
- try {
49
- new RegExp(p);
50
- } catch (err) {
51
- errors.push({
52
- index: i,
53
- pattern: p,
54
- message: (err as Error).message,
55
- });
56
- }
57
- }
58
- return errors;
59
- }
60
-
61
- function validateAllowlistFile(): AllowlistValidationError[] | null {
62
- const filePath = getAllowlistPath();
63
- if (!existsSync(filePath)) return null;
64
-
65
- const raw = readFileSync(filePath, "utf-8");
66
- const allowlistConfig: AllowlistConfig = JSON.parse(raw) as AllowlistConfig;
67
- return validateAllowlist(allowlistConfig);
68
- }
69
-
70
- function printUsage(): void {
71
- console.log("Usage: vellum config <subcommand> [options]");
72
- console.log("");
73
- console.log("Subcommands:");
74
- console.log(
75
- " get <key> Get a config value (supports dotted paths)",
76
- );
77
- console.log(
78
- " set <key> <value> Set a config value (supports dotted paths like apiKeys.anthropic)",
79
- );
80
- console.log(" list List all config values");
81
- console.log(
82
- " validate-allowlist Validate regex patterns in secret-allowlist.json",
83
- );
84
- }
85
-
86
- export function config(): void {
87
- const args = process.argv.slice(3);
88
- const subcommand = args[0];
89
-
90
- if (!subcommand || subcommand === "--help" || subcommand === "-h") {
91
- printUsage();
92
- return;
93
- }
94
-
95
- switch (subcommand) {
96
- case "set": {
97
- const key = args[1];
98
- const value = args[2];
99
- if (!key || value === undefined) {
100
- console.error("Usage: vellum config set <key> <value>");
101
- process.exit(1);
102
- }
103
- const raw = loadRawConfig();
104
- let parsed: unknown = value;
105
- try {
106
- parsed = JSON.parse(value);
107
- } catch {
108
- // keep as string
109
- }
110
- setNestedValue(raw, key, parsed);
111
- saveRawConfig(raw);
112
- syncConfigToLockfile();
113
- console.log(`Set ${key} = ${JSON.stringify(parsed)}`);
114
- break;
115
- }
116
-
117
- case "get": {
118
- const key = args[1];
119
- if (!key) {
120
- console.error("Usage: vellum config get <key>");
121
- process.exit(1);
122
- }
123
- const raw = loadRawConfig();
124
- const val = getNestedValue(raw, key);
125
- if (val === undefined) {
126
- console.log("(not set)");
127
- } else {
128
- console.log(
129
- typeof val === "object" ? JSON.stringify(val, null, 2) : String(val),
130
- );
131
- }
132
- break;
133
- }
134
-
135
- case "list": {
136
- const raw = loadRawConfig();
137
- if (Object.keys(raw).length === 0) {
138
- console.log("No configuration set");
139
- } else {
140
- console.log(JSON.stringify(raw, null, 2));
141
- }
142
- break;
143
- }
144
-
145
- case "validate-allowlist": {
146
- try {
147
- const errors = validateAllowlistFile();
148
- if (errors === null) {
149
- console.log("No secret-allowlist.json file found");
150
- return;
151
- }
152
- if (errors.length === 0) {
153
- console.log("All patterns in secret-allowlist.json are valid");
154
- return;
155
- }
156
- console.error(
157
- `Found ${errors.length} invalid pattern(s) in secret-allowlist.json:`,
158
- );
159
- for (const e of errors) {
160
- console.error(` [${e.index}] "${e.pattern}": ${e.message}`);
161
- }
162
- process.exit(1);
163
- } catch (err) {
164
- console.error(
165
- `Failed to read secret-allowlist.json: ${(err as Error).message}`,
166
- );
167
- process.exit(1);
168
- }
169
- break;
170
- }
171
-
172
- default: {
173
- console.error(`Unknown config subcommand: ${subcommand}`);
174
- printUsage();
175
- process.exit(1);
176
- }
177
- }
178
- }
@@ -1,241 +0,0 @@
1
- import { loadLatestAssistant } from "../lib/assistant-config";
2
- import { GATEWAY_PORT } from "../lib/constants.js";
3
-
4
- // ---------------------------------------------------------------------------
5
- // Gateway API client
6
- // ---------------------------------------------------------------------------
7
-
8
- function getGatewayUrl(): string {
9
- const entry = loadLatestAssistant();
10
- if (entry?.runtimeUrl) return entry.runtimeUrl;
11
- return `http://localhost:${GATEWAY_PORT}`;
12
- }
13
-
14
- function getBearerToken(): string | undefined {
15
- const entry = loadLatestAssistant();
16
- return entry?.bearerToken;
17
- }
18
-
19
- function buildHeaders(): Record<string, string> {
20
- const headers: Record<string, string> = {
21
- "Content-Type": "application/json",
22
- };
23
- const token = getBearerToken();
24
- if (token) {
25
- headers["Authorization"] = `Bearer ${token}`;
26
- }
27
- return headers;
28
- }
29
-
30
- async function apiGet(path: string): Promise<unknown> {
31
- const url = `${getGatewayUrl()}/v1/${path}`;
32
- const response = await fetch(url, { headers: buildHeaders() });
33
- if (!response.ok) {
34
- const text = await response.text();
35
- throw new Error(`API error ${response.status}: ${text}`);
36
- }
37
- return response.json();
38
- }
39
-
40
- async function apiPost(path: string, body: unknown): Promise<unknown> {
41
- const url = `${getGatewayUrl()}/v1/${path}`;
42
- const response = await fetch(url, {
43
- method: "POST",
44
- headers: buildHeaders(),
45
- body: JSON.stringify(body),
46
- });
47
- if (!response.ok) {
48
- const text = await response.text();
49
- throw new Error(`API error ${response.status}: ${text}`);
50
- }
51
- return response.json();
52
- }
53
-
54
- // ---------------------------------------------------------------------------
55
- // Types
56
- // ---------------------------------------------------------------------------
57
-
58
- interface ContactChannel {
59
- type: string;
60
- address: string;
61
- isPrimary: boolean;
62
- }
63
-
64
- interface Contact {
65
- id: string;
66
- displayName: string;
67
- notes: string | null;
68
- lastInteraction: number | null;
69
- interactionCount: number;
70
- channels: ContactChannel[];
71
- }
72
-
73
- // ---------------------------------------------------------------------------
74
- // Output helpers
75
- // ---------------------------------------------------------------------------
76
-
77
- function hasFlag(args: string[], flag: string): boolean {
78
- return args.includes(flag);
79
- }
80
-
81
- function getFlagValue(args: string[], flag: string): string | undefined {
82
- const idx = args.indexOf(flag);
83
- if (idx === -1 || idx + 1 >= args.length) return undefined;
84
- return args[idx + 1];
85
- }
86
-
87
- function formatContact(c: Contact): string {
88
- const lines = [
89
- ` ID: ${c.id}`,
90
- ` Name: ${c.displayName}`,
91
- ` Notes: ${c.notes ?? "(none)"}`,
92
- ` Interactions: ${c.interactionCount}`,
93
- ];
94
- if (c.lastInteraction) {
95
- lines.push(` Last seen: ${new Date(c.lastInteraction).toISOString()}`);
96
- }
97
- if (c.channels.length > 0) {
98
- lines.push(" Channels:");
99
- for (const ch of c.channels) {
100
- const primary = ch.isPrimary ? " (primary)" : "";
101
- lines.push(` - ${ch.type}: ${ch.address}${primary}`);
102
- }
103
- }
104
- return lines.join("\n");
105
- }
106
-
107
- // ---------------------------------------------------------------------------
108
- // Usage
109
- // ---------------------------------------------------------------------------
110
-
111
- function printUsage(): void {
112
- console.log("Usage: vellum contacts <subcommand> [options]");
113
- console.log("");
114
- console.log("Subcommands:");
115
- console.log(" list [--limit N] [--role ROLE] List all contacts");
116
- console.log(" get <id> Get a contact by ID");
117
- console.log(" merge <keepId> <mergeId> Merge two contacts");
118
- console.log("");
119
- console.log("Options:");
120
- console.log(" --json Machine-readable JSON output");
121
- }
122
-
123
- // ---------------------------------------------------------------------------
124
- // Command entry point
125
- // ---------------------------------------------------------------------------
126
-
127
- export async function contacts(): Promise<void> {
128
- const args = process.argv.slice(3);
129
- const subcommand = args[0];
130
- const json = hasFlag(args, "--json");
131
-
132
- if (!subcommand || subcommand === "--help" || subcommand === "-h") {
133
- printUsage();
134
- return;
135
- }
136
-
137
- switch (subcommand) {
138
- case "list": {
139
- const limit = getFlagValue(args, "--limit") ?? "50";
140
- const role = getFlagValue(args, "--role");
141
- const query = `contacts?limit=${limit}${role ? `&role=${encodeURIComponent(role)}` : ""}`;
142
- const data = (await apiGet(query)) as {
143
- ok: boolean;
144
- contacts: Contact[];
145
- };
146
-
147
- if (json) {
148
- console.log(JSON.stringify(data));
149
- return;
150
- }
151
-
152
- if (data.contacts.length === 0) {
153
- console.log("No contacts found.");
154
- return;
155
- }
156
-
157
- console.log(`Contacts (${data.contacts.length}):\n`);
158
- for (const c of data.contacts) {
159
- console.log(formatContact(c) + "\n");
160
- }
161
- break;
162
- }
163
-
164
- case "get": {
165
- const id = args[1];
166
- if (!id || id.startsWith("--")) {
167
- console.error("Usage: vellum contacts get <id>");
168
- process.exit(1);
169
- }
170
-
171
- try {
172
- const data = (await apiGet(`contacts/${encodeURIComponent(id)}`)) as {
173
- ok: boolean;
174
- contact: Contact;
175
- };
176
-
177
- if (json) {
178
- console.log(JSON.stringify(data));
179
- } else {
180
- console.log(formatContact(data.contact));
181
- }
182
- } catch {
183
- if (json) {
184
- console.log(
185
- JSON.stringify({
186
- ok: false,
187
- error: `Contact "${id}" not found`,
188
- }),
189
- );
190
- } else {
191
- console.error(`Contact "${id}" not found.`);
192
- }
193
- process.exitCode = 1;
194
- }
195
- break;
196
- }
197
-
198
- case "merge": {
199
- const keepId = args[1];
200
- const mergeId = args[2];
201
- if (
202
- !keepId ||
203
- !mergeId ||
204
- keepId.startsWith("--") ||
205
- mergeId.startsWith("--")
206
- ) {
207
- console.error("Usage: vellum contacts merge <keepId> <mergeId>");
208
- process.exit(1);
209
- }
210
-
211
- try {
212
- const data = (await apiPost("contacts/merge", {
213
- keepId,
214
- mergeId,
215
- })) as { ok: boolean; contact: Contact };
216
-
217
- if (json) {
218
- console.log(JSON.stringify(data));
219
- } else {
220
- console.log(`Merged contact "${mergeId}" into "${keepId}".\n`);
221
- console.log(formatContact(data.contact));
222
- }
223
- } catch (err) {
224
- const msg = err instanceof Error ? err.message : String(err);
225
- if (json) {
226
- console.log(JSON.stringify({ ok: false, error: msg }));
227
- } else {
228
- console.error(`Error: ${msg}`);
229
- }
230
- process.exitCode = 1;
231
- }
232
- break;
233
- }
234
-
235
- default: {
236
- console.error(`Unknown contacts subcommand: ${subcommand}`);
237
- printUsage();
238
- process.exit(1);
239
- }
240
- }
241
- }
@@ -1,108 +0,0 @@
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
- import { loadLatestAssistant } from "../lib/assistant-config.js";
11
-
12
- // ---------------------------------------------------------------------------
13
- // Helpers
14
- // ---------------------------------------------------------------------------
15
-
16
- function output(data: unknown): void {
17
- process.stdout.write(JSON.stringify(data, null, 2) + "\n");
18
- }
19
-
20
- function exitError(message: string): void {
21
- output({ ok: false, error: message });
22
- process.exitCode = 1;
23
- }
24
-
25
- // ---------------------------------------------------------------------------
26
- // Usage
27
- // ---------------------------------------------------------------------------
28
-
29
- function printUsage(): void {
30
- console.log(`Usage: vellum email <subcommand> [options]
31
-
32
- Subcommands:
33
- status Show email status (address, inboxes, callback URL)
34
- create <username> Create a new email inbox for the given username
35
-
36
- Options:
37
- --assistant <id> Assistant ID (defaults to the most recently hatched)
38
- --help, -h Show this help message
39
- `);
40
- }
41
-
42
- // ---------------------------------------------------------------------------
43
- // Entry point
44
- // ---------------------------------------------------------------------------
45
-
46
- export async function email(): Promise<void> {
47
- const args = process.argv.slice(3); // everything after "email"
48
-
49
- if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
50
- printUsage();
51
- return;
52
- }
53
-
54
- // Resolve assistant ID from --assistant flag or latest assistant
55
- const assistantFlagIdx = args.indexOf("--assistant");
56
- let assistantId: string | undefined;
57
- if (assistantFlagIdx !== -1) {
58
- const value = args[assistantFlagIdx + 1];
59
- if (!value || value.startsWith("-")) {
60
- exitError("--assistant requires a value.");
61
- return;
62
- }
63
- assistantId = value;
64
- args.splice(assistantFlagIdx, 2);
65
- }
66
- if (!assistantId) {
67
- assistantId = loadLatestAssistant()?.assistantId;
68
- }
69
- if (!assistantId) {
70
- exitError(
71
- "No assistant ID available. Pass --assistant <id> or hatch an assistant first.",
72
- );
73
- return;
74
- }
75
-
76
- const subcommand = args[0];
77
-
78
- switch (subcommand) {
79
- case "status": {
80
- try {
81
- const client = new VellumEmailClient(assistantId);
82
- const addresses = await client.status();
83
- output({ ok: true, addresses });
84
- } catch (err) {
85
- exitError(err instanceof Error ? err.message : String(err));
86
- }
87
- break;
88
- }
89
- case "create": {
90
- const username = args[1];
91
- if (!username) {
92
- exitError("Usage: vellum email create <username>");
93
- return;
94
- }
95
- try {
96
- const client = new VellumEmailClient(assistantId);
97
- const inbox = await client.createInbox(username);
98
- output({ ok: true, inbox });
99
- } catch (err) {
100
- exitError(err instanceof Error ? err.message : String(err));
101
- }
102
- break;
103
- }
104
- default:
105
- exitError(`Unknown email subcommand: ${subcommand}`);
106
- printUsage();
107
- }
108
- }
@@ -1,97 +0,0 @@
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 AssistantEmailAddress {
13
- id: string;
14
- address: string;
15
- created_at: string;
16
- }
17
-
18
- // ---------------------------------------------------------------------------
19
- // HTTP helper
20
- // ---------------------------------------------------------------------------
21
-
22
- async function vellumFetch(
23
- apiKey: string,
24
- baseUrl: string,
25
- path: string,
26
- opts: { method?: string; body?: unknown } = {},
27
- ): Promise<unknown> {
28
- const url = `${baseUrl}${path}`;
29
- const response = await fetch(url, {
30
- method: opts.method ?? "GET",
31
- headers: {
32
- Authorization: `Bearer ${apiKey}`,
33
- "Content-Type": "application/json",
34
- },
35
- body: opts.body ? JSON.stringify(opts.body) : undefined,
36
- });
37
-
38
- if (!response.ok) {
39
- const text = await response.text().catch(() => "");
40
- throw new Error(
41
- `Vellum email API error: ${response.status} ${response.statusText}${text ? ` — ${text}` : ""}`,
42
- );
43
- }
44
-
45
- const contentType = response.headers.get("content-type") ?? "";
46
- if (contentType.includes("application/json")) {
47
- return response.json();
48
- }
49
- return undefined;
50
- }
51
-
52
- // ---------------------------------------------------------------------------
53
- // Client
54
- // ---------------------------------------------------------------------------
55
-
56
- export class VellumEmailClient {
57
- private apiKey: string;
58
- private baseUrl: string;
59
- private assistantId: string;
60
-
61
- constructor(assistantId: string, apiKey?: string, baseUrl?: string) {
62
- this.assistantId = assistantId;
63
- const resolvedKey = apiKey ?? process.env.VELLUM_API_KEY;
64
- if (!resolvedKey) {
65
- throw new Error(
66
- "No Vellum API key configured. Set the VELLUM_API_KEY environment variable.",
67
- );
68
- }
69
- this.apiKey = resolvedKey;
70
- this.baseUrl =
71
- baseUrl ?? process.env.VELLUM_API_URL ?? DEFAULT_VELLUM_API_URL;
72
- }
73
-
74
- /** List existing email addresses and check connectivity. */
75
- async status(): Promise<AssistantEmailAddress[]> {
76
- const result = await vellumFetch(
77
- this.apiKey,
78
- this.baseUrl,
79
- `/v1/assistants/${this.assistantId}/email-addresses/`,
80
- );
81
- return result as AssistantEmailAddress[];
82
- }
83
-
84
- /** Provision a new email address for the given username. */
85
- async createInbox(username: string): Promise<AssistantEmailAddress> {
86
- const result = await vellumFetch(
87
- this.apiKey,
88
- this.baseUrl,
89
- `/v1/assistants/${this.assistantId}/email-addresses/`,
90
- {
91
- method: "POST",
92
- body: { username },
93
- },
94
- );
95
- return result as AssistantEmailAddress;
96
- }
97
- }
package/src/lib/config.ts DELETED
@@ -1,73 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
-
5
- function getRootDir(): string {
6
- return join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum");
7
- }
8
-
9
- export function getConfigPath(): string {
10
- return join(getRootDir(), "workspace", "config.json");
11
- }
12
-
13
- export function getAllowlistPath(): string {
14
- return join(getRootDir(), "protected", "secret-allowlist.json");
15
- }
16
-
17
- export function loadRawConfig(): Record<string, unknown> {
18
- const configPath = getConfigPath();
19
- if (!existsSync(configPath)) {
20
- return {};
21
- }
22
- const raw = readFileSync(configPath, "utf-8");
23
- return JSON.parse(raw) as Record<string, unknown>;
24
- }
25
-
26
- export function saveRawConfig(config: Record<string, unknown>): void {
27
- const configPath = getConfigPath();
28
- const dir = dirname(configPath);
29
- if (!existsSync(dir)) {
30
- mkdirSync(dir, { recursive: true });
31
- }
32
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
33
- }
34
-
35
- export function getNestedValue(
36
- obj: Record<string, unknown>,
37
- path: string,
38
- ): unknown {
39
- const keys = path.split(".");
40
- let current: unknown = obj;
41
- for (const key of keys) {
42
- if (
43
- current === null ||
44
- current === undefined ||
45
- typeof current !== "object"
46
- ) {
47
- return undefined;
48
- }
49
- current = (current as Record<string, unknown>)[key];
50
- }
51
- return current;
52
- }
53
-
54
- export function setNestedValue(
55
- obj: Record<string, unknown>,
56
- path: string,
57
- value: unknown,
58
- ): void {
59
- const keys = path.split(".");
60
- let current = obj;
61
- for (let i = 0; i < keys.length - 1; i++) {
62
- const key = keys[i];
63
- if (
64
- current[key] === undefined ||
65
- current[key] === null ||
66
- typeof current[key] !== "object"
67
- ) {
68
- current[key] = {};
69
- }
70
- current = current[key] as Record<string, unknown>;
71
- }
72
- current[keys[keys.length - 1]] = value;
73
- }