@vellumai/cli 0.4.35 → 0.4.37
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 +47 -0
- package/package.json +2 -2
- package/src/__tests__/skills-uninstall.test.ts +201 -0
- package/src/commands/skills.ts +130 -5
- package/src/index.ts +2 -10
- package/src/lib/local.ts +3 -0
- package/src/lib/ngrok.ts +24 -1
- package/src/commands/autonomy.ts +0 -320
- package/src/commands/config.ts +0 -178
- package/src/commands/contacts.ts +0 -241
- package/src/commands/email.ts +0 -108
- package/src/email/vellum.ts +0 -97
- package/src/lib/config.ts +0 -73
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.
|
|
3
|
+
"version": "0.4.37",
|
|
4
4
|
"description": "CLI tools for vellum-assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"./src/commands/*": "./src/commands/*.ts"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
|
-
"
|
|
14
|
+
"assistant": "./src/index.ts"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"format": "prettier --write .",
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { skills } from "../commands/skills.js";
|
|
13
|
+
|
|
14
|
+
let tempDir: string;
|
|
15
|
+
let originalArgv: string[];
|
|
16
|
+
let originalBaseDataDir: string | undefined;
|
|
17
|
+
let originalExitCode: number | string | null | undefined;
|
|
18
|
+
|
|
19
|
+
function getSkillsDir(): string {
|
|
20
|
+
return join(tempDir, ".vellum", "workspace", "skills");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getSkillsIndexPath(): string {
|
|
24
|
+
return join(getSkillsDir(), "SKILLS.md");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function installFakeSkill(skillId: string): void {
|
|
28
|
+
const skillDir = join(getSkillsDir(), skillId);
|
|
29
|
+
mkdirSync(skillDir, { recursive: true });
|
|
30
|
+
writeFileSync(join(skillDir, "SKILL.md"), `# ${skillId}\nA test skill.\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeSkillsIndex(content: string): void {
|
|
34
|
+
mkdirSync(getSkillsDir(), { recursive: true });
|
|
35
|
+
writeFileSync(getSkillsIndexPath(), content);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
tempDir = join(tmpdir(), `skills-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
40
|
+
mkdirSync(join(tempDir, ".vellum", "workspace", "skills"), {
|
|
41
|
+
recursive: true,
|
|
42
|
+
});
|
|
43
|
+
originalArgv = process.argv;
|
|
44
|
+
originalBaseDataDir = process.env.BASE_DATA_DIR;
|
|
45
|
+
originalExitCode = process.exitCode;
|
|
46
|
+
process.env.BASE_DATA_DIR = tempDir;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
process.argv = originalArgv;
|
|
51
|
+
process.env.BASE_DATA_DIR = originalBaseDataDir;
|
|
52
|
+
process.exitCode = originalExitCode;
|
|
53
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("vellum skills uninstall", () => {
|
|
57
|
+
test("removes skill directory and SKILLS.md entry", async () => {
|
|
58
|
+
/**
|
|
59
|
+
* Tests the happy path for uninstalling a skill.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
// GIVEN a skill is installed locally
|
|
63
|
+
installFakeSkill("weather");
|
|
64
|
+
writeSkillsIndex("- weather\n- google-oauth-setup\n");
|
|
65
|
+
|
|
66
|
+
// WHEN we run `vellum skills uninstall weather`
|
|
67
|
+
process.argv = ["bun", "run", "skills", "uninstall", "weather"];
|
|
68
|
+
await skills();
|
|
69
|
+
|
|
70
|
+
// THEN the skill directory should be removed
|
|
71
|
+
expect(existsSync(join(getSkillsDir(), "weather"))).toBe(false);
|
|
72
|
+
|
|
73
|
+
// AND the SKILLS.md entry should be removed
|
|
74
|
+
const index = readFileSync(getSkillsIndexPath(), "utf-8");
|
|
75
|
+
expect(index).not.toContain("weather");
|
|
76
|
+
|
|
77
|
+
// AND other skills should remain in the index
|
|
78
|
+
expect(index).toContain("google-oauth-setup");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("outputs JSON on success when --json flag is passed", async () => {
|
|
82
|
+
/**
|
|
83
|
+
* Tests that --json flag produces machine-readable output.
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
// GIVEN a skill is installed locally
|
|
87
|
+
installFakeSkill("weather");
|
|
88
|
+
writeSkillsIndex("- weather\n");
|
|
89
|
+
|
|
90
|
+
// WHEN we run `vellum skills uninstall weather --json`
|
|
91
|
+
process.argv = ["bun", "run", "skills", "uninstall", "weather", "--json"];
|
|
92
|
+
const logs: string[] = [];
|
|
93
|
+
const origLog = console.log;
|
|
94
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "));
|
|
95
|
+
try {
|
|
96
|
+
await skills();
|
|
97
|
+
} finally {
|
|
98
|
+
console.log = origLog;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// THEN JSON output should indicate success
|
|
102
|
+
const output = JSON.parse(logs[0]);
|
|
103
|
+
expect(output).toEqual({ ok: true, skillId: "weather" });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("errors when skill is not installed", async () => {
|
|
107
|
+
/**
|
|
108
|
+
* Tests that uninstalling a non-existent skill produces an error.
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
// GIVEN no skills are installed
|
|
112
|
+
// WHEN we run `vellum skills uninstall nonexistent`
|
|
113
|
+
process.argv = ["bun", "run", "skills", "uninstall", "nonexistent"];
|
|
114
|
+
const errors: string[] = [];
|
|
115
|
+
const origError = console.error;
|
|
116
|
+
console.error = (...args: unknown[]) => errors.push(args.join(" "));
|
|
117
|
+
try {
|
|
118
|
+
await skills();
|
|
119
|
+
} finally {
|
|
120
|
+
console.error = origError;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// THEN an error message should be displayed
|
|
124
|
+
expect(errors[0]).toContain('Skill "nonexistent" is not installed.');
|
|
125
|
+
expect(process.exitCode).toBe(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("errors with JSON output when skill is not installed and --json is passed", async () => {
|
|
129
|
+
/**
|
|
130
|
+
* Tests that --json flag produces machine-readable error output.
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
// GIVEN no skills are installed
|
|
134
|
+
// WHEN we run `vellum skills uninstall nonexistent --json`
|
|
135
|
+
process.argv = [
|
|
136
|
+
"bun",
|
|
137
|
+
"run",
|
|
138
|
+
"skills",
|
|
139
|
+
"uninstall",
|
|
140
|
+
"nonexistent",
|
|
141
|
+
"--json",
|
|
142
|
+
];
|
|
143
|
+
const logs: string[] = [];
|
|
144
|
+
const origLog = console.log;
|
|
145
|
+
console.log = (...args: unknown[]) => logs.push(args.join(" "));
|
|
146
|
+
try {
|
|
147
|
+
await skills();
|
|
148
|
+
} finally {
|
|
149
|
+
console.log = origLog;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// THEN JSON output should indicate failure
|
|
153
|
+
const output = JSON.parse(logs[0]);
|
|
154
|
+
expect(output.ok).toBe(false);
|
|
155
|
+
expect(output.error).toContain('Skill "nonexistent" is not installed.');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("works when SKILLS.md does not exist", async () => {
|
|
159
|
+
/**
|
|
160
|
+
* Tests that uninstall works even if the SKILLS.md index file is missing.
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
// GIVEN a skill directory exists but no SKILLS.md
|
|
164
|
+
installFakeSkill("weather");
|
|
165
|
+
|
|
166
|
+
// WHEN we run `vellum skills uninstall weather`
|
|
167
|
+
process.argv = ["bun", "run", "skills", "uninstall", "weather"];
|
|
168
|
+
await skills();
|
|
169
|
+
|
|
170
|
+
// THEN the skill directory should be removed
|
|
171
|
+
expect(existsSync(join(getSkillsDir(), "weather"))).toBe(false);
|
|
172
|
+
|
|
173
|
+
// AND no SKILLS.md should have been created
|
|
174
|
+
expect(existsSync(getSkillsIndexPath())).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("removes skill with nested files", async () => {
|
|
178
|
+
/**
|
|
179
|
+
* Tests that uninstall recursively removes skills with nested directories.
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
// GIVEN a skill with nested files is installed
|
|
183
|
+
const skillDir = join(getSkillsDir(), "weather");
|
|
184
|
+
mkdirSync(join(skillDir, "scripts", "lib"), { recursive: true });
|
|
185
|
+
writeFileSync(join(skillDir, "SKILL.md"), "# weather\n");
|
|
186
|
+
writeFileSync(join(skillDir, "scripts", "fetch.sh"), "#!/bin/bash\n");
|
|
187
|
+
writeFileSync(join(skillDir, "scripts", "lib", "utils.sh"), "# utils\n");
|
|
188
|
+
writeSkillsIndex("- weather\n");
|
|
189
|
+
|
|
190
|
+
// WHEN we run `vellum skills uninstall weather`
|
|
191
|
+
process.argv = ["bun", "run", "skills", "uninstall", "weather"];
|
|
192
|
+
await skills();
|
|
193
|
+
|
|
194
|
+
// THEN the entire skill directory tree should be removed
|
|
195
|
+
expect(existsSync(skillDir)).toBe(false);
|
|
196
|
+
|
|
197
|
+
// AND the SKILLS.md entry should be removed
|
|
198
|
+
const index = readFileSync(getSkillsIndexPath(), "utf-8");
|
|
199
|
+
expect(index).not.toContain("weather");
|
|
200
|
+
});
|
|
201
|
+
});
|
package/src/commands/skills.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import {
|
|
4
|
+
cpSync,
|
|
4
5
|
existsSync,
|
|
5
6
|
mkdirSync,
|
|
6
7
|
readFileSync,
|
|
7
8
|
renameSync,
|
|
9
|
+
rmSync,
|
|
8
10
|
writeFileSync,
|
|
9
11
|
} from "node:fs";
|
|
10
12
|
import { homedir } from "node:os";
|
|
@@ -27,6 +29,38 @@ function getSkillsIndexPath(): string {
|
|
|
27
29
|
return join(getSkillsDir(), "SKILLS.md");
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the repo-level skills/ directory when running in dev mode.
|
|
34
|
+
* Returns the path if VELLUM_DEV is set and the directory exists, or undefined.
|
|
35
|
+
*/
|
|
36
|
+
function getRepoSkillsDir(): string | undefined {
|
|
37
|
+
if (!process.env.VELLUM_DEV) return undefined;
|
|
38
|
+
|
|
39
|
+
// cli/src/commands/skills.ts -> ../../../skills/
|
|
40
|
+
const candidate = join(import.meta.dir, "..", "..", "..", "skills");
|
|
41
|
+
if (existsSync(join(candidate, "catalog.json"))) {
|
|
42
|
+
return candidate;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read skills from the repo-local catalog.json.
|
|
49
|
+
*/
|
|
50
|
+
function readLocalCatalog(repoSkillsDir: string): CatalogSkill[] {
|
|
51
|
+
try {
|
|
52
|
+
const raw = readFileSync(
|
|
53
|
+
join(repoSkillsDir, "catalog.json"),
|
|
54
|
+
"utf-8",
|
|
55
|
+
);
|
|
56
|
+
const manifest = JSON.parse(raw) as CatalogManifest;
|
|
57
|
+
if (!Array.isArray(manifest.skills)) return [];
|
|
58
|
+
return manifest.skills;
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
30
64
|
// ---------------------------------------------------------------------------
|
|
31
65
|
// Platform API client
|
|
32
66
|
// ---------------------------------------------------------------------------
|
|
@@ -221,6 +255,33 @@ function upsertSkillsIndex(id: string): void {
|
|
|
221
255
|
atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
|
|
222
256
|
}
|
|
223
257
|
|
|
258
|
+
function removeSkillsIndexEntry(id: string): void {
|
|
259
|
+
const indexPath = getSkillsIndexPath();
|
|
260
|
+
if (!existsSync(indexPath)) return;
|
|
261
|
+
|
|
262
|
+
const lines = readFileSync(indexPath, "utf-8").split("\n");
|
|
263
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
264
|
+
const pattern = new RegExp(`^[-*]\\s+(?:\`)?${escaped}(?:\`)?\\s*$`);
|
|
265
|
+
const filtered = lines.filter((line) => !pattern.test(line));
|
|
266
|
+
|
|
267
|
+
// If nothing changed, skip the write
|
|
268
|
+
if (filtered.length === lines.length) return;
|
|
269
|
+
|
|
270
|
+
const content = filtered.join("\n");
|
|
271
|
+
atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function uninstallSkillLocally(skillId: string): void {
|
|
275
|
+
const skillDir = join(getSkillsDir(), skillId);
|
|
276
|
+
|
|
277
|
+
if (!existsSync(skillDir)) {
|
|
278
|
+
throw new Error(`Skill "${skillId}" is not installed.`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
282
|
+
removeSkillsIndexEntry(skillId);
|
|
283
|
+
}
|
|
284
|
+
|
|
224
285
|
async function installSkillLocally(
|
|
225
286
|
skillId: string,
|
|
226
287
|
catalogEntry: CatalogSkill,
|
|
@@ -237,8 +298,17 @@ async function installSkillLocally(
|
|
|
237
298
|
|
|
238
299
|
mkdirSync(skillDir, { recursive: true });
|
|
239
300
|
|
|
240
|
-
//
|
|
241
|
-
|
|
301
|
+
// In dev mode, install from the local repo skills directory if available
|
|
302
|
+
const repoSkillsDir = getRepoSkillsDir();
|
|
303
|
+
const repoSkillSource = repoSkillsDir
|
|
304
|
+
? join(repoSkillsDir, skillId)
|
|
305
|
+
: undefined;
|
|
306
|
+
|
|
307
|
+
if (repoSkillSource && existsSync(join(repoSkillSource, "SKILL.md"))) {
|
|
308
|
+
cpSync(repoSkillSource, skillDir, { recursive: true });
|
|
309
|
+
} else {
|
|
310
|
+
await fetchAndExtractSkill(skillId, skillDir);
|
|
311
|
+
}
|
|
242
312
|
|
|
243
313
|
// Write version metadata
|
|
244
314
|
if (catalogEntry.version) {
|
|
@@ -288,6 +358,9 @@ function printUsage(): void {
|
|
|
288
358
|
console.log(
|
|
289
359
|
" install <skill-id> [--overwrite] Install a skill from the catalog",
|
|
290
360
|
);
|
|
361
|
+
console.log(
|
|
362
|
+
" uninstall <skill-id> Uninstall a previously installed skill",
|
|
363
|
+
);
|
|
291
364
|
console.log("");
|
|
292
365
|
console.log("Options:");
|
|
293
366
|
console.log(" --json Machine-readable JSON output");
|
|
@@ -312,6 +385,18 @@ export async function skills(): Promise<void> {
|
|
|
312
385
|
try {
|
|
313
386
|
const catalog = await fetchCatalog();
|
|
314
387
|
|
|
388
|
+
// In dev mode, merge in skills from the repo-local skills/ directory
|
|
389
|
+
const repoSkillsDir = getRepoSkillsDir();
|
|
390
|
+
if (repoSkillsDir) {
|
|
391
|
+
const localSkills = readLocalCatalog(repoSkillsDir);
|
|
392
|
+
const remoteIds = new Set(catalog.map((s) => s.id));
|
|
393
|
+
for (const local of localSkills) {
|
|
394
|
+
if (!remoteIds.has(local.id)) {
|
|
395
|
+
catalog.push(local);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
315
400
|
if (json) {
|
|
316
401
|
console.log(JSON.stringify({ ok: true, skills: catalog }));
|
|
317
402
|
return;
|
|
@@ -353,9 +438,20 @@ export async function skills(): Promise<void> {
|
|
|
353
438
|
const overwrite = hasFlag(args, "--overwrite");
|
|
354
439
|
|
|
355
440
|
try {
|
|
356
|
-
//
|
|
357
|
-
const
|
|
358
|
-
|
|
441
|
+
// In dev mode, also check the repo-local skills/ directory
|
|
442
|
+
const repoSkillsDir = getRepoSkillsDir();
|
|
443
|
+
let localSkills: CatalogSkill[] = [];
|
|
444
|
+
if (repoSkillsDir) {
|
|
445
|
+
localSkills = readLocalCatalog(repoSkillsDir);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Check local catalog first, then fall back to remote
|
|
449
|
+
let entry = localSkills.find((s) => s.id === skillId);
|
|
450
|
+
if (!entry) {
|
|
451
|
+
const catalog = await fetchCatalog();
|
|
452
|
+
entry = catalog.find((s) => s.id === skillId);
|
|
453
|
+
}
|
|
454
|
+
|
|
359
455
|
if (!entry) {
|
|
360
456
|
throw new Error(`Skill "${skillId}" not found in the Vellum catalog`);
|
|
361
457
|
}
|
|
@@ -380,6 +476,35 @@ export async function skills(): Promise<void> {
|
|
|
380
476
|
break;
|
|
381
477
|
}
|
|
382
478
|
|
|
479
|
+
case "uninstall": {
|
|
480
|
+
const skillId = args.find(
|
|
481
|
+
(a) => !a.startsWith("--") && a !== "uninstall",
|
|
482
|
+
);
|
|
483
|
+
if (!skillId) {
|
|
484
|
+
console.error("Usage: vellum skills uninstall <skill-id>");
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
uninstallSkillLocally(skillId);
|
|
490
|
+
|
|
491
|
+
if (json) {
|
|
492
|
+
console.log(JSON.stringify({ ok: true, skillId }));
|
|
493
|
+
} else {
|
|
494
|
+
console.log(`Uninstalled skill "${skillId}".`);
|
|
495
|
+
}
|
|
496
|
+
} catch (err) {
|
|
497
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
498
|
+
if (json) {
|
|
499
|
+
console.log(JSON.stringify({ ok: false, error: msg }));
|
|
500
|
+
} else {
|
|
501
|
+
console.error(`Error: ${msg}`);
|
|
502
|
+
}
|
|
503
|
+
process.exitCode = 1;
|
|
504
|
+
}
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
|
|
383
508
|
default: {
|
|
384
509
|
console.error(`Unknown skills subcommand: ${subcommand}`);
|
|
385
510
|
printUsage();
|
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
|
|
92
|
-
console.log(" email Email operations (
|
|
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
|
@@ -342,6 +342,7 @@ async function startDaemonWatchFromSource(
|
|
|
342
342
|
const env: Record<string, string | undefined> = {
|
|
343
343
|
...process.env,
|
|
344
344
|
RUNTIME_HTTP_PORT: process.env.RUNTIME_HTTP_PORT || "7821",
|
|
345
|
+
VELLUM_DEV: "1",
|
|
345
346
|
};
|
|
346
347
|
|
|
347
348
|
const daemonLogFd = openLogFile("hatch.log");
|
|
@@ -701,6 +702,7 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
701
702
|
"VELLUM_DAEMON_TCP_PORT",
|
|
702
703
|
"VELLUM_DAEMON_TCP_HOST",
|
|
703
704
|
"VELLUM_DAEMON_SOCKET",
|
|
705
|
+
"VELLUM_KEYCHAIN_BROKER_SOCKET",
|
|
704
706
|
"VELLUM_DEBUG",
|
|
705
707
|
"SENTRY_DSN",
|
|
706
708
|
"TMPDIR",
|
|
@@ -839,6 +841,7 @@ export async function startGateway(
|
|
|
839
841
|
// `vellum sleep` to SIGKILL the gateway when the CLI timeout is shorter
|
|
840
842
|
// than the drain window. Respect an explicit env override.
|
|
841
843
|
GATEWAY_SHUTDOWN_DRAIN_MS: process.env.GATEWAY_SHUTDOWN_DRAIN_MS || "0",
|
|
844
|
+
...(watch ? { VELLUM_DEV: "1" } : {}),
|
|
842
845
|
};
|
|
843
846
|
|
|
844
847
|
if (process.env.GATEWAY_UNMAPPED_POLICY) {
|
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;
|