vellum-cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -5
- package/dist/index.js +275 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,17 +17,41 @@ vellum push --help
|
|
|
17
17
|
Requires Node 18+. The package is self-contained (`@vellum/core` is bundled in),
|
|
18
18
|
so it has no runtime dependencies.
|
|
19
19
|
|
|
20
|
+
## Authentication
|
|
21
|
+
|
|
22
|
+
The normal path is `vellum login`, which signs you in through your browser and
|
|
23
|
+
stores a per-user token under `~/.config/vellum/config.json`
|
|
24
|
+
(`$XDG_CONFIG_HOME` is honored). After that, commands authenticate as you with
|
|
25
|
+
no further setup.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
vellum login # opens the browser, approve, done
|
|
29
|
+
vellum whoami # who am I logged in as?
|
|
30
|
+
vellum logout # revoke + forget this machine's token
|
|
31
|
+
```
|
|
32
|
+
|
|
20
33
|
## Configuration
|
|
21
34
|
|
|
22
|
-
|
|
35
|
+
`--url` defaults to the hosted instance (`https://vellumai.app`); override it
|
|
36
|
+
for self-hosted servers. Connection settings come from flags, falling back to
|
|
37
|
+
env vars:
|
|
38
|
+
|
|
39
|
+
| Flag | Env var | Description |
|
|
40
|
+
| ------------ | ---------------- | -------------------------------------------- |
|
|
41
|
+
| `--url` | `VELLUM_URL` | Server base URL (default `https://vellumai.app`) |
|
|
42
|
+
| `--api-key` | `VELLUM_API_KEY` | Shared owner/agent key (`X-Api-Key`) |
|
|
23
43
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
| `--api-key` | `VELLUM_API_KEY` | Shared API key (`X-Api-Key`) |
|
|
44
|
+
Credential precedence for a write: an explicit `--api-key`/`VELLUM_API_KEY`
|
|
45
|
+
(the shared owner key, e.g. for agents/CI) wins; otherwise the stored
|
|
46
|
+
`vellum login` token is used.
|
|
28
47
|
|
|
29
48
|
## Commands
|
|
30
49
|
|
|
50
|
+
### `vellum login`
|
|
51
|
+
|
|
52
|
+
Authenticate this machine. Opens the browser to approve, polls until you do,
|
|
53
|
+
then stores the token. `--no-browser` just prints the URL to open manually.
|
|
54
|
+
|
|
31
55
|
### `vellum push [file]`
|
|
32
56
|
|
|
33
57
|
Create an artifact from HTML. Reads from `<file>` if given, otherwise from
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,67 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/config.ts
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
var DEFAULT_URL = "https://vellumai.app";
|
|
8
|
+
|
|
4
9
|
class ConfigError extends Error {
|
|
5
10
|
}
|
|
6
|
-
function
|
|
7
|
-
|
|
11
|
+
function normalizeUrl(url) {
|
|
12
|
+
return url.replace(/\/+$/, "");
|
|
13
|
+
}
|
|
14
|
+
function resolveBaseUrl(flags) {
|
|
15
|
+
return normalizeUrl(flags.url ?? process.env.VELLUM_URL ?? DEFAULT_URL);
|
|
16
|
+
}
|
|
17
|
+
async function resolveCredential(flags, baseUrl) {
|
|
8
18
|
const apiKey = flags.apiKey ?? process.env.VELLUM_API_KEY;
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
19
|
+
if (apiKey)
|
|
20
|
+
return { apiKey };
|
|
21
|
+
const token = await loadToken(baseUrl);
|
|
22
|
+
if (token)
|
|
23
|
+
return { token };
|
|
24
|
+
throw new ConfigError(`Not logged in to ${baseUrl}. Run \`vellum login\` (or set VELLUM_API_KEY / pass --api-key).`);
|
|
25
|
+
}
|
|
26
|
+
function storePath() {
|
|
27
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
28
|
+
const base = xdg && xdg.trim().length > 0 ? xdg : join(homedir(), ".config");
|
|
29
|
+
return join(base, "vellum", "config.json");
|
|
30
|
+
}
|
|
31
|
+
async function readStore() {
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readFile(storePath(), "utf8");
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
return { servers: parsed.servers ?? {} };
|
|
36
|
+
} catch {
|
|
37
|
+
return { servers: {} };
|
|
14
38
|
}
|
|
15
|
-
|
|
39
|
+
}
|
|
40
|
+
async function writeStore(store) {
|
|
41
|
+
const path = storePath();
|
|
42
|
+
await mkdir(dirname(path), { recursive: true });
|
|
43
|
+
await writeFile(path, `${JSON.stringify(store, null, 2)}
|
|
44
|
+
`, { mode: 384 });
|
|
45
|
+
}
|
|
46
|
+
async function loadToken(baseUrl) {
|
|
47
|
+
const store = await readStore();
|
|
48
|
+
return store.servers[normalizeUrl(baseUrl)]?.token;
|
|
49
|
+
}
|
|
50
|
+
async function saveToken(baseUrl, token, email) {
|
|
51
|
+
const store = await readStore();
|
|
52
|
+
store.servers[normalizeUrl(baseUrl)] = { token, email };
|
|
53
|
+
await writeStore(store);
|
|
54
|
+
}
|
|
55
|
+
async function clearToken(baseUrl) {
|
|
56
|
+
const store = await readStore();
|
|
57
|
+
const key = normalizeUrl(baseUrl);
|
|
58
|
+
const existing = store.servers[key]?.token;
|
|
59
|
+
delete store.servers[key];
|
|
60
|
+
await writeStore(store);
|
|
61
|
+
return existing;
|
|
16
62
|
}
|
|
17
63
|
|
|
18
|
-
// src/commands/
|
|
19
|
-
import { readFile } from "node:fs/promises";
|
|
64
|
+
// src/commands/login.ts
|
|
20
65
|
import { parseArgs } from "node:util";
|
|
21
66
|
// ../core/src/client.ts
|
|
22
67
|
class VellumApiError extends Error {
|
|
@@ -33,12 +78,25 @@ class VellumApiError extends Error {
|
|
|
33
78
|
class VellumClient {
|
|
34
79
|
baseUrl;
|
|
35
80
|
apiKey;
|
|
81
|
+
token;
|
|
36
82
|
fetchImpl;
|
|
37
83
|
constructor(opts) {
|
|
38
84
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
39
85
|
this.apiKey = opts.apiKey;
|
|
86
|
+
this.token = opts.token;
|
|
40
87
|
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
41
88
|
}
|
|
89
|
+
authHeaders() {
|
|
90
|
+
if (this.token)
|
|
91
|
+
return { Authorization: `Bearer ${this.token}` };
|
|
92
|
+
if (this.apiKey)
|
|
93
|
+
return { "X-Api-Key": this.apiKey };
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
async fail(res) {
|
|
97
|
+
const code = await res.clone().json().then((b) => b.error).catch(() => res.statusText);
|
|
98
|
+
throw new VellumApiError(res.status, code);
|
|
99
|
+
}
|
|
42
100
|
async createPage(html, opts = {}) {
|
|
43
101
|
const url = new URL(`${this.baseUrl}/v1/pages`);
|
|
44
102
|
if (opts.pageId)
|
|
@@ -47,19 +105,168 @@ class VellumClient {
|
|
|
47
105
|
method: "POST",
|
|
48
106
|
headers: {
|
|
49
107
|
"Content-Type": "text/html; charset=utf-8",
|
|
50
|
-
|
|
108
|
+
...this.authHeaders()
|
|
51
109
|
},
|
|
52
110
|
body: html
|
|
53
111
|
});
|
|
54
|
-
if (!res.ok)
|
|
55
|
-
|
|
56
|
-
throw new VellumApiError(res.status, code);
|
|
57
|
-
}
|
|
112
|
+
if (!res.ok)
|
|
113
|
+
await this.fail(res);
|
|
58
114
|
return await res.json();
|
|
59
115
|
}
|
|
116
|
+
async startCliAuth() {
|
|
117
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/cli/auth/start`, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { "Content-Type": "application/json" },
|
|
120
|
+
body: "{}"
|
|
121
|
+
});
|
|
122
|
+
if (!res.ok)
|
|
123
|
+
await this.fail(res);
|
|
124
|
+
return await res.json();
|
|
125
|
+
}
|
|
126
|
+
async pollCliAuth(deviceCode) {
|
|
127
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/cli/auth/poll`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "Content-Type": "application/json" },
|
|
130
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
131
|
+
});
|
|
132
|
+
if (!res.ok)
|
|
133
|
+
await this.fail(res);
|
|
134
|
+
return await res.json();
|
|
135
|
+
}
|
|
136
|
+
async whoami() {
|
|
137
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/cli/whoami`, {
|
|
138
|
+
headers: this.authHeaders()
|
|
139
|
+
});
|
|
140
|
+
if (!res.ok)
|
|
141
|
+
await this.fail(res);
|
|
142
|
+
return await res.json();
|
|
143
|
+
}
|
|
144
|
+
async logout() {
|
|
145
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/cli/auth/logout`, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: this.authHeaders()
|
|
148
|
+
});
|
|
149
|
+
if (!res.ok)
|
|
150
|
+
await this.fail(res);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// src/util/open.ts
|
|
154
|
+
import { spawn } from "node:child_process";
|
|
155
|
+
function openBrowser(url) {
|
|
156
|
+
const platform = process.platform;
|
|
157
|
+
const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
158
|
+
try {
|
|
159
|
+
const child = spawn(cmd, args, {
|
|
160
|
+
stdio: "ignore",
|
|
161
|
+
detached: true
|
|
162
|
+
});
|
|
163
|
+
child.on("error", () => {});
|
|
164
|
+
child.unref();
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/commands/login.ts
|
|
169
|
+
var HELP = `vellum login — authenticate this machine to a Vellum server
|
|
170
|
+
|
|
171
|
+
Opens your browser to approve a CLI login, then stores a token under
|
|
172
|
+
~/.config/vellum/config.json so future commands authenticate as you.
|
|
173
|
+
|
|
174
|
+
Usage:
|
|
175
|
+
vellum login [options]
|
|
176
|
+
|
|
177
|
+
Options:
|
|
178
|
+
--url <url> Server base URL (env: VELLUM_URL)
|
|
179
|
+
--no-browser Don't auto-open the browser; just print the URL
|
|
180
|
+
-h, --help Show this help`;
|
|
181
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
182
|
+
async function loginCommand(argv) {
|
|
183
|
+
const { values } = parseArgs({
|
|
184
|
+
args: argv,
|
|
185
|
+
options: {
|
|
186
|
+
url: { type: "string" },
|
|
187
|
+
"no-browser": { type: "boolean", default: false },
|
|
188
|
+
help: { type: "boolean", short: "h", default: false }
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
if (values.help) {
|
|
192
|
+
console.log(HELP);
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
const baseUrl = resolveBaseUrl({ url: values.url });
|
|
196
|
+
const client2 = new VellumClient({ baseUrl });
|
|
197
|
+
const start = await client2.startCliAuth();
|
|
198
|
+
console.log(`
|
|
199
|
+
To authorize this CLI, visit:
|
|
200
|
+
`);
|
|
201
|
+
console.log(` ${start.verification_uri_complete}
|
|
202
|
+
`);
|
|
203
|
+
console.log(`and confirm the code: ${start.user_code}
|
|
204
|
+
`);
|
|
205
|
+
if (!values["no-browser"]) {
|
|
206
|
+
openBrowser(start.verification_uri_complete);
|
|
207
|
+
console.log(`(Opening your browser…)
|
|
208
|
+
`);
|
|
209
|
+
}
|
|
210
|
+
process.stdout.write("Waiting for approval");
|
|
211
|
+
const deadline = Date.now() + start.expires_in * 1000;
|
|
212
|
+
const intervalMs = Math.max(1, start.interval) * 1000;
|
|
213
|
+
while (Date.now() < deadline) {
|
|
214
|
+
await sleep(intervalMs);
|
|
215
|
+
process.stdout.write(".");
|
|
216
|
+
const res = await client2.pollCliAuth(start.device_code);
|
|
217
|
+
if (res.status === "complete") {
|
|
218
|
+
await saveToken(baseUrl, res.token, res.email);
|
|
219
|
+
console.log(`
|
|
220
|
+
|
|
221
|
+
✓ Logged in to ${baseUrl}${res.email ? ` as ${res.email}` : ""}.`);
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
if (res.status === "expired")
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
console.error("\n\nError: login request expired. Run `vellum login` again.");
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/commands/logout.ts
|
|
232
|
+
import { parseArgs as parseArgs2 } from "node:util";
|
|
233
|
+
var HELP2 = `vellum logout — remove this machine's stored CLI token
|
|
234
|
+
|
|
235
|
+
Usage:
|
|
236
|
+
vellum logout [options]
|
|
237
|
+
|
|
238
|
+
Options:
|
|
239
|
+
--url <url> Server base URL (env: VELLUM_URL)
|
|
240
|
+
-h, --help Show this help`;
|
|
241
|
+
async function logoutCommand(argv) {
|
|
242
|
+
const { values } = parseArgs2({
|
|
243
|
+
args: argv,
|
|
244
|
+
options: {
|
|
245
|
+
url: { type: "string" },
|
|
246
|
+
help: { type: "boolean", short: "h", default: false }
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
if (values.help) {
|
|
250
|
+
console.log(HELP2);
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
const baseUrl = resolveBaseUrl({ url: values.url });
|
|
254
|
+
const token = await clearToken(baseUrl);
|
|
255
|
+
if (!token) {
|
|
256
|
+
console.log(`Not logged in to ${baseUrl}.`);
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
await new VellumClient({ baseUrl, token }).logout();
|
|
261
|
+
} catch {}
|
|
262
|
+
console.log(`✓ Logged out of ${baseUrl}.`);
|
|
263
|
+
return 0;
|
|
60
264
|
}
|
|
265
|
+
|
|
61
266
|
// src/commands/push.ts
|
|
62
|
-
|
|
267
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
268
|
+
import { parseArgs as parseArgs3 } from "node:util";
|
|
269
|
+
var HELP3 = `vellum push — create a Vellum artifact from HTML
|
|
63
270
|
|
|
64
271
|
Usage:
|
|
65
272
|
vellum push <file.html> [options]
|
|
@@ -78,7 +285,7 @@ async function readStdin() {
|
|
|
78
285
|
return Buffer.concat(chunks).toString("utf8");
|
|
79
286
|
}
|
|
80
287
|
async function pushCommand(argv) {
|
|
81
|
-
const { values, positionals } =
|
|
288
|
+
const { values, positionals } = parseArgs3({
|
|
82
289
|
args: argv,
|
|
83
290
|
allowPositionals: true,
|
|
84
291
|
options: {
|
|
@@ -90,20 +297,19 @@ async function pushCommand(argv) {
|
|
|
90
297
|
}
|
|
91
298
|
});
|
|
92
299
|
if (values.help) {
|
|
93
|
-
console.log(
|
|
300
|
+
console.log(HELP3);
|
|
94
301
|
return 0;
|
|
95
302
|
}
|
|
96
303
|
const file = positionals[0];
|
|
97
|
-
const html = file ? await
|
|
304
|
+
const html = file ? await readFile2(file, "utf8") : await readStdin();
|
|
98
305
|
if (!html.trim()) {
|
|
99
306
|
console.error("Error: no HTML provided (empty file or stdin).");
|
|
100
307
|
return 1;
|
|
101
308
|
}
|
|
102
|
-
const {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
});
|
|
106
|
-
const client2 = new VellumClient({ baseUrl, apiKey });
|
|
309
|
+
const flags = { url: values.url, apiKey: values["api-key"] };
|
|
310
|
+
const baseUrl = resolveBaseUrl(flags);
|
|
311
|
+
const credential = await resolveCredential(flags, baseUrl);
|
|
312
|
+
const client2 = new VellumClient({ baseUrl, ...credential });
|
|
107
313
|
const res = await client2.createPage(html, { pageId: values["page-id"] });
|
|
108
314
|
if (values.json) {
|
|
109
315
|
console.log(JSON.stringify(res, null, 2));
|
|
@@ -115,30 +321,72 @@ async function pushCommand(argv) {
|
|
|
115
321
|
return 0;
|
|
116
322
|
}
|
|
117
323
|
|
|
324
|
+
// src/commands/whoami.ts
|
|
325
|
+
import { parseArgs as parseArgs4 } from "node:util";
|
|
326
|
+
var HELP4 = `vellum whoami — show the identity the CLI is authenticated as
|
|
327
|
+
|
|
328
|
+
Usage:
|
|
329
|
+
vellum whoami [options]
|
|
330
|
+
|
|
331
|
+
Options:
|
|
332
|
+
--url <url> Server base URL (env: VELLUM_URL)
|
|
333
|
+
--api-key <key> Shared API key (env: VELLUM_API_KEY)
|
|
334
|
+
-h, --help Show this help`;
|
|
335
|
+
async function whoamiCommand(argv) {
|
|
336
|
+
const { values } = parseArgs4({
|
|
337
|
+
args: argv,
|
|
338
|
+
options: {
|
|
339
|
+
url: { type: "string" },
|
|
340
|
+
"api-key": { type: "string" },
|
|
341
|
+
help: { type: "boolean", short: "h", default: false }
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
if (values.help) {
|
|
345
|
+
console.log(HELP4);
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
const baseUrl = resolveBaseUrl({ url: values.url });
|
|
349
|
+
const credential = await resolveCredential({ url: values.url, apiKey: values["api-key"] }, baseUrl);
|
|
350
|
+
if ("apiKey" in credential) {
|
|
351
|
+
console.log(`Authenticated to ${baseUrl} with a shared API key (owner).`);
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
const client2 = new VellumClient({ baseUrl, token: credential.token });
|
|
355
|
+
const { email } = await client2.whoami();
|
|
356
|
+
console.log(`${email ?? "(no email on record)"} — ${baseUrl}`);
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
118
360
|
// src/index.ts
|
|
119
|
-
var
|
|
361
|
+
var HELP5 = `vellum — CLI for the Vellum artifact store
|
|
120
362
|
|
|
121
363
|
Usage:
|
|
122
364
|
vellum <command> [options]
|
|
123
365
|
|
|
124
366
|
Commands:
|
|
367
|
+
login Authenticate this machine (opens your browser)
|
|
368
|
+
logout Remove this machine's stored CLI token
|
|
369
|
+
whoami Show the identity the CLI is authenticated as
|
|
125
370
|
push Create an artifact (page) from HTML (file or stdin)
|
|
126
371
|
|
|
127
372
|
Run 'vellum <command> --help' for command-specific options.`;
|
|
128
373
|
var commands = {
|
|
374
|
+
login: loginCommand,
|
|
375
|
+
logout: logoutCommand,
|
|
376
|
+
whoami: whoamiCommand,
|
|
129
377
|
push: pushCommand
|
|
130
378
|
};
|
|
131
379
|
async function main() {
|
|
132
380
|
const [, , cmd, ...rest] = process.argv;
|
|
133
381
|
if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") {
|
|
134
|
-
console.log(
|
|
382
|
+
console.log(HELP5);
|
|
135
383
|
return cmd ? 0 : 1;
|
|
136
384
|
}
|
|
137
385
|
const handler = commands[cmd];
|
|
138
386
|
if (!handler) {
|
|
139
387
|
console.error(`Unknown command: ${cmd}
|
|
140
388
|
`);
|
|
141
|
-
console.error(
|
|
389
|
+
console.error(HELP5);
|
|
142
390
|
return 1;
|
|
143
391
|
}
|
|
144
392
|
return handler(rest);
|