vellum-cli 0.1.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +29 -5
  2. package/dist/index.js +761 -29
  3. 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
- Connection settings come from flags, falling back to env vars:
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
- | Flag | Env var | Description |
25
- | ------------ | ---------------- | ---------------------------- |
26
- | `--url` | `VELLUM_URL` | Server base URL |
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 resolveConfig(flags) {
7
- const baseUrl = flags.url ?? process.env.VELLUM_URL;
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 (!baseUrl) {
10
- throw new ConfigError("Missing server URL. Pass --url or set VELLUM_URL.");
11
- }
12
- if (!apiKey) {
13
- throw new ConfigError("Missing API key. Pass --api-key or set VELLUM_API_KEY.");
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
- return { baseUrl, apiKey };
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/push.ts
19
- import { readFile } from "node:fs/promises";
64
+ // src/commands/comments.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,506 @@ class VellumClient {
47
105
  method: "POST",
48
106
  headers: {
49
107
  "Content-Type": "text/html; charset=utf-8",
50
- "X-Api-Key": this.apiKey
108
+ ...this.authHeaders()
51
109
  },
52
110
  body: html
53
111
  });
54
- if (!res.ok) {
55
- const code = await res.json().then((b) => b.error).catch(() => res.statusText);
56
- throw new VellumApiError(res.status, code);
57
- }
112
+ if (!res.ok)
113
+ await this.fail(res);
114
+ return await res.json();
115
+ }
116
+ async getPage(pageId) {
117
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/pages/${encodeURIComponent(pageId)}`, { headers: this.authHeaders() });
118
+ if (!res.ok)
119
+ await this.fail(res);
120
+ return await res.json();
121
+ }
122
+ async createComment(pageId, opts) {
123
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/pages/${encodeURIComponent(pageId)}/comments`, {
124
+ method: "POST",
125
+ headers: { "Content-Type": "application/json", ...this.authHeaders() },
126
+ body: JSON.stringify({
127
+ version_id: opts.versionId,
128
+ body: opts.body,
129
+ anchor: opts.anchor,
130
+ parent_id: opts.parentId
131
+ })
132
+ });
133
+ if (!res.ok)
134
+ await this.fail(res);
135
+ return await res.json();
136
+ }
137
+ async listComments(pageId, opts = {}) {
138
+ const url = new URL(`${this.baseUrl}/v1/pages/${encodeURIComponent(pageId)}/comments`);
139
+ if (opts.versionId)
140
+ url.searchParams.set("version_id", opts.versionId);
141
+ const res = await this.fetchImpl(url, { headers: this.authHeaders() });
142
+ if (!res.ok)
143
+ await this.fail(res);
144
+ return await res.json();
145
+ }
146
+ async resolveComment(commentId) {
147
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/comments/${encodeURIComponent(commentId)}/resolve`, { method: "POST", headers: this.authHeaders() });
148
+ if (!res.ok)
149
+ await this.fail(res);
150
+ return await res.json();
151
+ }
152
+ async startCliAuth() {
153
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/cli/auth/start`, {
154
+ method: "POST",
155
+ headers: { "Content-Type": "application/json" },
156
+ body: "{}"
157
+ });
158
+ if (!res.ok)
159
+ await this.fail(res);
160
+ return await res.json();
161
+ }
162
+ async pollCliAuth(deviceCode) {
163
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/cli/auth/poll`, {
164
+ method: "POST",
165
+ headers: { "Content-Type": "application/json" },
166
+ body: JSON.stringify({ device_code: deviceCode })
167
+ });
168
+ if (!res.ok)
169
+ await this.fail(res);
58
170
  return await res.json();
59
171
  }
172
+ async whoami() {
173
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/cli/whoami`, {
174
+ headers: this.authHeaders()
175
+ });
176
+ if (!res.ok)
177
+ await this.fail(res);
178
+ return await res.json();
179
+ }
180
+ async logout() {
181
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/cli/auth/logout`, {
182
+ method: "POST",
183
+ headers: this.authHeaders()
184
+ });
185
+ if (!res.ok)
186
+ await this.fail(res);
187
+ }
188
+ }
189
+ // src/commands/comments.ts
190
+ var HELP = `vellum comments — list the comments on a document
191
+
192
+ Usage:
193
+ vellum comments <page-id> [options]
194
+
195
+ Reads are public, so no login is required. Comments print as threads; replies
196
+ are indented under their parent.
197
+
198
+ Options:
199
+ --open Show only unresolved threads
200
+ --version <id> Only comments anchored to this version id
201
+ --url <url> Server base URL (env: VELLUM_URL)
202
+ --json Print the raw JSON response
203
+ -h, --help Show this help`;
204
+ var truncate = (s, n) => s.length > n ? `${s.slice(0, n - 1)}…` : s;
205
+ var oneLine = (s) => s.replace(/\s+/g, " ").trim();
206
+ function printComment(c, indent) {
207
+ const who = c.author_email ?? c.author_name ?? "(unknown)";
208
+ const state = c.resolved ? "resolved" : "open";
209
+ console.log(`${indent}${c.id} [${state}] ${who}`);
210
+ if (c.anchor?.exact) {
211
+ console.log(`${indent} on “${truncate(oneLine(c.anchor.exact), 70)}”`);
212
+ }
213
+ console.log(`${indent} ${truncate(oneLine(c.body), 100)}`);
214
+ for (const reply of c.replies ?? []) {
215
+ printComment(reply, `${indent} `);
216
+ }
217
+ }
218
+ async function commentsCommand(argv) {
219
+ const { values, positionals } = parseArgs({
220
+ args: argv,
221
+ allowPositionals: true,
222
+ options: {
223
+ open: { type: "boolean", default: false },
224
+ version: { type: "string" },
225
+ url: { type: "string" },
226
+ json: { type: "boolean", default: false },
227
+ help: { type: "boolean", short: "h", default: false }
228
+ }
229
+ });
230
+ if (values.help) {
231
+ console.log(HELP);
232
+ return 0;
233
+ }
234
+ const pageId = positionals[0];
235
+ if (!pageId) {
236
+ console.error(`Error: missing <page-id>.
237
+ `);
238
+ console.error(HELP);
239
+ return 1;
240
+ }
241
+ const baseUrl = resolveBaseUrl({ url: values.url });
242
+ const client2 = new VellumClient({ baseUrl });
243
+ let comments = await client2.listComments(pageId, {
244
+ versionId: values.version
245
+ });
246
+ if (values.open)
247
+ comments = comments.filter((c) => !c.resolved);
248
+ if (values.json) {
249
+ console.log(JSON.stringify(comments, null, 2));
250
+ return 0;
251
+ }
252
+ if (comments.length === 0) {
253
+ console.log(values.open ? "No open comments." : "No comments.");
254
+ return 0;
255
+ }
256
+ for (const c of comments)
257
+ printComment(c, "");
258
+ return 0;
259
+ }
260
+
261
+ // src/commands/get.ts
262
+ import { parseArgs as parseArgs2 } from "node:util";
263
+ var HELP2 = `vellum get — print a document's raw HTML
264
+
265
+ Usage:
266
+ vellum get <page-id> [options]
267
+
268
+ Reads are public, so no login is required. Without --version, the page's
269
+ current version is printed.
270
+
271
+ Options:
272
+ --version <n> Print a specific version number (default: current)
273
+ --url <url> Server base URL (env: VELLUM_URL)
274
+ -h, --help Show this help`;
275
+ function resolveVersion(page, requested) {
276
+ if (requested != null) {
277
+ return page.versions.find((v) => v.version_number === requested) ?? null;
278
+ }
279
+ const current = page.current_version_id ? page.versions.find((v) => v.id === page.current_version_id) : undefined;
280
+ return current ?? page.versions[page.versions.length - 1] ?? null;
60
281
  }
282
+ async function getCommand(argv) {
283
+ const { values, positionals } = parseArgs2({
284
+ args: argv,
285
+ allowPositionals: true,
286
+ options: {
287
+ version: { type: "string" },
288
+ url: { type: "string" },
289
+ help: { type: "boolean", short: "h", default: false }
290
+ }
291
+ });
292
+ if (values.help) {
293
+ console.log(HELP2);
294
+ return 0;
295
+ }
296
+ const pageId = positionals[0];
297
+ if (!pageId) {
298
+ console.error(`Error: missing <page-id>.
299
+ `);
300
+ console.error(HELP2);
301
+ return 1;
302
+ }
303
+ let version;
304
+ if (values.version != null) {
305
+ version = Number.parseInt(values.version, 10);
306
+ if (!Number.isInteger(version) || version < 1) {
307
+ console.error("Error: --version must be a positive integer.");
308
+ return 1;
309
+ }
310
+ }
311
+ const baseUrl = resolveBaseUrl({ url: values.url });
312
+ const client2 = new VellumClient({ baseUrl });
313
+ const page = await client2.getPage(pageId);
314
+ const target = resolveVersion(page, version);
315
+ if (!target) {
316
+ console.error(version != null ? `Error: page has no version ${version}.` : "Error: page has no versions.");
317
+ return 1;
318
+ }
319
+ const res = await fetch(target.raw_url);
320
+ if (!res.ok) {
321
+ console.error(`Error: could not fetch raw HTML (${res.status}).`);
322
+ return 1;
323
+ }
324
+ process.stdout.write(await res.text());
325
+ return 0;
326
+ }
327
+
328
+ // src/commands/login.ts
329
+ import { parseArgs as parseArgs3 } from "node:util";
330
+
331
+ // src/util/open.ts
332
+ import { spawn } from "node:child_process";
333
+ function openBrowser(url) {
334
+ const platform = process.platform;
335
+ const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
336
+ try {
337
+ const child = spawn(cmd, args, {
338
+ stdio: "ignore",
339
+ detached: true
340
+ });
341
+ child.on("error", () => {});
342
+ child.unref();
343
+ } catch {}
344
+ }
345
+
346
+ // src/commands/login.ts
347
+ var HELP3 = `vellum login — authenticate this machine to a Vellum server
348
+
349
+ Opens your browser to approve a CLI login, then stores a token under
350
+ ~/.config/vellum/config.json so future commands authenticate as you.
351
+
352
+ Usage:
353
+ vellum login [options]
354
+
355
+ Options:
356
+ --url <url> Server base URL (env: VELLUM_URL)
357
+ --no-browser Don't auto-open the browser; just print the URL
358
+ -h, --help Show this help`;
359
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
360
+ async function loginCommand(argv) {
361
+ const { values } = parseArgs3({
362
+ args: argv,
363
+ options: {
364
+ url: { type: "string" },
365
+ "no-browser": { type: "boolean", default: false },
366
+ help: { type: "boolean", short: "h", default: false }
367
+ }
368
+ });
369
+ if (values.help) {
370
+ console.log(HELP3);
371
+ return 0;
372
+ }
373
+ const baseUrl = resolveBaseUrl({ url: values.url });
374
+ const client2 = new VellumClient({ baseUrl });
375
+ const start = await client2.startCliAuth();
376
+ console.log(`
377
+ To authorize this CLI, visit:
378
+ `);
379
+ console.log(` ${start.verification_uri_complete}
380
+ `);
381
+ console.log(`and confirm the code: ${start.user_code}
382
+ `);
383
+ if (!values["no-browser"]) {
384
+ openBrowser(start.verification_uri_complete);
385
+ console.log(`(Opening your browser…)
386
+ `);
387
+ }
388
+ process.stdout.write("Waiting for approval");
389
+ const deadline = Date.now() + start.expires_in * 1000;
390
+ const intervalMs = Math.max(1, start.interval) * 1000;
391
+ while (Date.now() < deadline) {
392
+ await sleep(intervalMs);
393
+ process.stdout.write(".");
394
+ const res = await client2.pollCliAuth(start.device_code);
395
+ if (res.status === "complete") {
396
+ await saveToken(baseUrl, res.token, res.email);
397
+ console.log(`
398
+
399
+ ✓ Logged in to ${baseUrl}${res.email ? ` as ${res.email}` : ""}.`);
400
+ return 0;
401
+ }
402
+ if (res.status === "expired")
403
+ break;
404
+ }
405
+ console.error("\n\nError: login request expired. Run `vellum login` again.");
406
+ return 1;
407
+ }
408
+
409
+ // src/commands/logout.ts
410
+ import { parseArgs as parseArgs4 } from "node:util";
411
+ var HELP4 = `vellum logout — remove this machine's stored CLI token
412
+
413
+ Usage:
414
+ vellum logout [options]
415
+
416
+ Options:
417
+ --url <url> Server base URL (env: VELLUM_URL)
418
+ -h, --help Show this help`;
419
+ async function logoutCommand(argv) {
420
+ const { values } = parseArgs4({
421
+ args: argv,
422
+ options: {
423
+ url: { type: "string" },
424
+ help: { type: "boolean", short: "h", default: false }
425
+ }
426
+ });
427
+ if (values.help) {
428
+ console.log(HELP4);
429
+ return 0;
430
+ }
431
+ const baseUrl = resolveBaseUrl({ url: values.url });
432
+ const token = await clearToken(baseUrl);
433
+ if (!token) {
434
+ console.log(`Not logged in to ${baseUrl}.`);
435
+ return 0;
436
+ }
437
+ try {
438
+ await new VellumClient({ baseUrl, token }).logout();
439
+ } catch {}
440
+ console.log(`✓ Logged out of ${baseUrl}.`);
441
+ return 0;
442
+ }
443
+
444
+ // src/commands/markup.ts
445
+ import { parseArgs as parseArgs5 } from "node:util";
446
+ var HELP5 = `vellum markup — pin a comment to a passage of a document
447
+
448
+ Usage:
449
+ vellum markup <page-id> --quote "<text>" --body "<note>" [options]
450
+ echo "<note>" | vellum markup <page-id> --quote "<text>"
451
+
452
+ The --quote text must appear verbatim in the document; the viewer highlights it
453
+ and links the highlight to your note. Without --version, the page's current
454
+ version is used.
455
+
456
+ Options:
457
+ --quote <text> Passage to mark up (must match the document text)
458
+ --body <note> Your note (or pipe it via stdin)
459
+ --version <n> Pin to a specific version number (default: current)
460
+ --force Post even if the quote isn't found in the version
461
+ --url <url> Server base URL (env: VELLUM_URL)
462
+ --api-key <key> Shared API key (env: VELLUM_API_KEY)
463
+ --json Print the raw JSON response
464
+ -h, --help Show this help`;
465
+ async function readStdin() {
466
+ const chunks = [];
467
+ for await (const chunk of process.stdin)
468
+ chunks.push(chunk);
469
+ return Buffer.concat(chunks).toString("utf8");
470
+ }
471
+ function resolveVersion2(page, requested) {
472
+ if (requested != null) {
473
+ return page.versions.find((v) => v.version_number === requested) ?? null;
474
+ }
475
+ const current = page.current_version_id ? page.versions.find((v) => v.id === page.current_version_id) : undefined;
476
+ return current ?? page.versions[page.versions.length - 1] ?? null;
477
+ }
478
+ function extractText(html) {
479
+ return html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
480
+ }
481
+ var collapse = (s) => s.replace(/\s+/g, " ").trim();
482
+ function quoteIsAnchorable(html, quote) {
483
+ const text = extractText(html);
484
+ return text.includes(quote) || collapse(text).includes(collapse(quote));
485
+ }
486
+ async function markupCommand(argv) {
487
+ const { values, positionals } = parseArgs5({
488
+ args: argv,
489
+ allowPositionals: true,
490
+ options: {
491
+ quote: { type: "string" },
492
+ body: { type: "string" },
493
+ version: { type: "string" },
494
+ force: { type: "boolean", default: false },
495
+ url: { type: "string" },
496
+ "api-key": { type: "string" },
497
+ json: { type: "boolean", default: false },
498
+ help: { type: "boolean", short: "h", default: false }
499
+ }
500
+ });
501
+ if (values.help) {
502
+ console.log(HELP5);
503
+ return 0;
504
+ }
505
+ const pageId = positionals[0];
506
+ if (!pageId) {
507
+ console.error(`Error: missing <page-id>.
508
+ `);
509
+ console.error(HELP5);
510
+ return 1;
511
+ }
512
+ const quote = (values.quote ?? "").trim();
513
+ if (!quote) {
514
+ console.error("Error: --quote is required (the passage to mark up).");
515
+ return 1;
516
+ }
517
+ const body = (values.body ?? await readStdin()).trim();
518
+ if (!body) {
519
+ console.error("Error: no note provided (pass --body or pipe it via stdin).");
520
+ return 1;
521
+ }
522
+ let version;
523
+ if (values.version != null) {
524
+ version = Number.parseInt(values.version, 10);
525
+ if (!Number.isInteger(version) || version < 1) {
526
+ console.error("Error: --version must be a positive integer.");
527
+ return 1;
528
+ }
529
+ }
530
+ const flags = { url: values.url, apiKey: values["api-key"] };
531
+ const baseUrl = resolveBaseUrl(flags);
532
+ const credential = await resolveCredential(flags, baseUrl);
533
+ const client2 = new VellumClient({ baseUrl, ...credential });
534
+ const page = await client2.getPage(pageId);
535
+ const target = resolveVersion2(page, version);
536
+ if (!target) {
537
+ console.error(version != null ? `Error: page has no version ${version}.` : "Error: page has no versions to mark up.");
538
+ return 1;
539
+ }
540
+ if (!values.force) {
541
+ const html = await fetch(target.raw_url).then((r) => r.ok ? r.text() : "");
542
+ if (!quoteIsAnchorable(html, quote)) {
543
+ console.error(`Error: --quote was not found in v${target.version_number}; the highlight
544
+ ` + " would not appear. Check the wording, or pass --force to post anyway.");
545
+ return 1;
546
+ }
547
+ }
548
+ const anchor = { type: "text-quote", exact: quote };
549
+ const comment = await client2.createComment(pageId, {
550
+ versionId: target.id,
551
+ body,
552
+ anchor
553
+ });
554
+ if (values.json) {
555
+ console.log(JSON.stringify(comment, null, 2));
556
+ } else {
557
+ console.log("✓ markup added");
558
+ console.log(` on: “${quote.length > 80 ? `${quote.slice(0, 79)}…` : quote}”`);
559
+ console.log(` view: ${page.view_url}`);
560
+ }
561
+ return 0;
562
+ }
563
+
564
+ // src/commands/open.ts
565
+ import { parseArgs as parseArgs6 } from "node:util";
566
+ var HELP6 = `vellum open — print (and open) a document's view URL
567
+
568
+ Usage:
569
+ vellum open <page-id> [options]
570
+
571
+ Options:
572
+ --no-browser Just print the URL; don't open the browser
573
+ --url <url> Server base URL (env: VELLUM_URL)
574
+ -h, --help Show this help`;
575
+ async function openCommand(argv) {
576
+ const { values, positionals } = parseArgs6({
577
+ args: argv,
578
+ allowPositionals: true,
579
+ options: {
580
+ "no-browser": { type: "boolean", default: false },
581
+ url: { type: "string" },
582
+ help: { type: "boolean", short: "h", default: false }
583
+ }
584
+ });
585
+ if (values.help) {
586
+ console.log(HELP6);
587
+ return 0;
588
+ }
589
+ const pageId = positionals[0];
590
+ if (!pageId) {
591
+ console.error(`Error: missing <page-id>.
592
+ `);
593
+ console.error(HELP6);
594
+ return 1;
595
+ }
596
+ const baseUrl = resolveBaseUrl({ url: values.url });
597
+ const page = await new VellumClient({ baseUrl }).getPage(pageId);
598
+ console.log(page.view_url);
599
+ if (!values["no-browser"])
600
+ openBrowser(page.view_url);
601
+ return 0;
602
+ }
603
+
61
604
  // src/commands/push.ts
62
- var HELP = `vellum push create a Vellum artifact from HTML
605
+ import { readFile as readFile2 } from "node:fs/promises";
606
+ import { parseArgs as parseArgs7 } from "node:util";
607
+ var HELP7 = `vellum push — create a Vellum artifact from HTML
63
608
 
64
609
  Usage:
65
610
  vellum push <file.html> [options]
@@ -71,14 +616,14 @@ Options:
71
616
  --api-key <key> Shared API key (env: VELLUM_API_KEY)
72
617
  --json Print the raw JSON response
73
618
  -h, --help Show this help`;
74
- async function readStdin() {
619
+ async function readStdin2() {
75
620
  const chunks = [];
76
621
  for await (const chunk of process.stdin)
77
622
  chunks.push(chunk);
78
623
  return Buffer.concat(chunks).toString("utf8");
79
624
  }
80
625
  async function pushCommand(argv) {
81
- const { values, positionals } = parseArgs({
626
+ const { values, positionals } = parseArgs7({
82
627
  args: argv,
83
628
  allowPositionals: true,
84
629
  options: {
@@ -90,20 +635,19 @@ async function pushCommand(argv) {
90
635
  }
91
636
  });
92
637
  if (values.help) {
93
- console.log(HELP);
638
+ console.log(HELP7);
94
639
  return 0;
95
640
  }
96
641
  const file = positionals[0];
97
- const html = file ? await readFile(file, "utf8") : await readStdin();
642
+ const html = file ? await readFile2(file, "utf8") : await readStdin2();
98
643
  if (!html.trim()) {
99
644
  console.error("Error: no HTML provided (empty file or stdin).");
100
645
  return 1;
101
646
  }
102
- const { baseUrl, apiKey } = resolveConfig({
103
- url: values.url,
104
- apiKey: values["api-key"]
105
- });
106
- const client2 = new VellumClient({ baseUrl, apiKey });
647
+ const flags = { url: values.url, apiKey: values["api-key"] };
648
+ const baseUrl = resolveBaseUrl(flags);
649
+ const credential = await resolveCredential(flags, baseUrl);
650
+ const client2 = new VellumClient({ baseUrl, ...credential });
107
651
  const res = await client2.createPage(html, { pageId: values["page-id"] });
108
652
  if (values.json) {
109
653
  console.log(JSON.stringify(res, null, 2));
@@ -115,30 +659,218 @@ async function pushCommand(argv) {
115
659
  return 0;
116
660
  }
117
661
 
662
+ // src/commands/reply.ts
663
+ import { parseArgs as parseArgs8 } from "node:util";
664
+ var HELP8 = `vellum reply — reply to a comment thread
665
+
666
+ Usage:
667
+ vellum reply <page-id> <comment-id> --body "<note>"
668
+ echo "<note>" | vellum reply <page-id> <comment-id>
669
+
670
+ The reply is threaded under <comment-id> and inherits the parent's version.
671
+ Run \`vellum comments <page-id>\` to find the page and comment ids.
672
+
673
+ Options:
674
+ --body <note> Your reply (or pipe it via stdin)
675
+ --url <url> Server base URL (env: VELLUM_URL)
676
+ --api-key <key> Shared API key (env: VELLUM_API_KEY)
677
+ --json Print the raw JSON response
678
+ -h, --help Show this help`;
679
+ async function readStdin3() {
680
+ const chunks = [];
681
+ for await (const chunk of process.stdin)
682
+ chunks.push(chunk);
683
+ return Buffer.concat(chunks).toString("utf8");
684
+ }
685
+ function flatten(comments) {
686
+ const out = [];
687
+ const walk = (c) => {
688
+ out.push(c);
689
+ for (const reply of c.replies ?? [])
690
+ walk(reply);
691
+ };
692
+ for (const c of comments)
693
+ walk(c);
694
+ return out;
695
+ }
696
+ async function replyCommand(argv) {
697
+ const { values, positionals } = parseArgs8({
698
+ args: argv,
699
+ allowPositionals: true,
700
+ options: {
701
+ body: { type: "string" },
702
+ url: { type: "string" },
703
+ "api-key": { type: "string" },
704
+ json: { type: "boolean", default: false },
705
+ help: { type: "boolean", short: "h", default: false }
706
+ }
707
+ });
708
+ if (values.help) {
709
+ console.log(HELP8);
710
+ return 0;
711
+ }
712
+ const [pageId, commentId] = positionals;
713
+ if (!pageId || !commentId) {
714
+ console.error("Error: usage is `vellum reply <page-id> <comment-id>`.\n");
715
+ console.error(HELP8);
716
+ return 1;
717
+ }
718
+ const body = (values.body ?? await readStdin3()).trim();
719
+ if (!body) {
720
+ console.error("Error: no note provided (pass --body or pipe it via stdin).");
721
+ return 1;
722
+ }
723
+ const flags = { url: values.url, apiKey: values["api-key"] };
724
+ const baseUrl = resolveBaseUrl(flags);
725
+ const credential = await resolveCredential(flags, baseUrl);
726
+ const client2 = new VellumClient({ baseUrl, ...credential });
727
+ const parent = flatten(await client2.listComments(pageId)).find((c) => c.id === commentId);
728
+ if (!parent) {
729
+ console.error(`Error: no comment ${commentId} on page ${pageId}.`);
730
+ return 1;
731
+ }
732
+ if (!parent.version_id) {
733
+ console.error("Error: parent comment has no version to reply on.");
734
+ return 1;
735
+ }
736
+ const reply = await client2.createComment(pageId, {
737
+ versionId: parent.version_id,
738
+ body,
739
+ parentId: commentId
740
+ });
741
+ if (values.json) {
742
+ console.log(JSON.stringify(reply, null, 2));
743
+ } else {
744
+ console.log(`✓ replied to ${commentId}`);
745
+ }
746
+ return 0;
747
+ }
748
+
749
+ // src/commands/resolve.ts
750
+ import { parseArgs as parseArgs9 } from "node:util";
751
+ var HELP9 = `vellum resolve — mark a comment as resolved
752
+
753
+ Usage:
754
+ vellum resolve <comment-id> [options]
755
+
756
+ Options:
757
+ --url <url> Server base URL (env: VELLUM_URL)
758
+ --api-key <key> Shared API key (env: VELLUM_API_KEY)
759
+ --json Print the raw JSON response
760
+ -h, --help Show this help`;
761
+ async function resolveCommand(argv) {
762
+ const { values, positionals } = parseArgs9({
763
+ args: argv,
764
+ allowPositionals: true,
765
+ options: {
766
+ url: { type: "string" },
767
+ "api-key": { type: "string" },
768
+ json: { type: "boolean", default: false },
769
+ help: { type: "boolean", short: "h", default: false }
770
+ }
771
+ });
772
+ if (values.help) {
773
+ console.log(HELP9);
774
+ return 0;
775
+ }
776
+ const commentId = positionals[0];
777
+ if (!commentId) {
778
+ console.error(`Error: missing <comment-id>.
779
+ `);
780
+ console.error(HELP9);
781
+ return 1;
782
+ }
783
+ const flags = { url: values.url, apiKey: values["api-key"] };
784
+ const baseUrl = resolveBaseUrl(flags);
785
+ const credential = await resolveCredential(flags, baseUrl);
786
+ const client2 = new VellumClient({ baseUrl, ...credential });
787
+ const comment = await client2.resolveComment(commentId);
788
+ if (values.json) {
789
+ console.log(JSON.stringify(comment, null, 2));
790
+ } else {
791
+ console.log(`✓ resolved ${comment.id}`);
792
+ }
793
+ return 0;
794
+ }
795
+
796
+ // src/commands/whoami.ts
797
+ import { parseArgs as parseArgs10 } from "node:util";
798
+ var HELP10 = `vellum whoami — show the identity the CLI is authenticated as
799
+
800
+ Usage:
801
+ vellum whoami [options]
802
+
803
+ Options:
804
+ --url <url> Server base URL (env: VELLUM_URL)
805
+ --api-key <key> Shared API key (env: VELLUM_API_KEY)
806
+ -h, --help Show this help`;
807
+ async function whoamiCommand(argv) {
808
+ const { values } = parseArgs10({
809
+ args: argv,
810
+ options: {
811
+ url: { type: "string" },
812
+ "api-key": { type: "string" },
813
+ help: { type: "boolean", short: "h", default: false }
814
+ }
815
+ });
816
+ if (values.help) {
817
+ console.log(HELP10);
818
+ return 0;
819
+ }
820
+ const baseUrl = resolveBaseUrl({ url: values.url });
821
+ const credential = await resolveCredential({ url: values.url, apiKey: values["api-key"] }, baseUrl);
822
+ if ("apiKey" in credential) {
823
+ console.log(`Authenticated to ${baseUrl} with a shared API key (owner).`);
824
+ return 0;
825
+ }
826
+ const client2 = new VellumClient({ baseUrl, token: credential.token });
827
+ const { email } = await client2.whoami();
828
+ console.log(`${email ?? "(no email on record)"} — ${baseUrl}`);
829
+ return 0;
830
+ }
831
+
118
832
  // src/index.ts
119
- var HELP2 = `vellum — CLI for the Vellum artifact store
833
+ var HELP11 = `vellum — CLI for the Vellum artifact store
120
834
 
121
835
  Usage:
122
836
  vellum <command> [options]
123
837
 
124
838
  Commands:
839
+ login Authenticate this machine (opens your browser)
840
+ logout Remove this machine's stored CLI token
841
+ whoami Show the identity the CLI is authenticated as
125
842
  push Create an artifact (page) from HTML (file or stdin)
843
+ get Print a document's raw HTML
844
+ open Print (and open) a document's view URL
845
+ markup Pin a comment to a passage of a document
846
+ comments List the comments on a document
847
+ reply Reply to a comment thread
848
+ resolve Mark a comment as resolved
126
849
 
127
850
  Run 'vellum <command> --help' for command-specific options.`;
128
851
  var commands = {
129
- push: pushCommand
852
+ login: loginCommand,
853
+ logout: logoutCommand,
854
+ whoami: whoamiCommand,
855
+ push: pushCommand,
856
+ get: getCommand,
857
+ open: openCommand,
858
+ markup: markupCommand,
859
+ comments: commentsCommand,
860
+ reply: replyCommand,
861
+ resolve: resolveCommand
130
862
  };
131
863
  async function main() {
132
864
  const [, , cmd, ...rest] = process.argv;
133
865
  if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") {
134
- console.log(HELP2);
866
+ console.log(HELP11);
135
867
  return cmd ? 0 : 1;
136
868
  }
137
869
  const handler = commands[cmd];
138
870
  if (!handler) {
139
871
  console.error(`Unknown command: ${cmd}
140
872
  `);
141
- console.error(HELP2);
873
+ console.error(HELP11);
142
874
  return 1;
143
875
  }
144
876
  return handler(rest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vellum-cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "The vellum CLI — publish agent-authored HTML artifacts to a Vellum server.",
5
5
  "type": "module",
6
6
  "license": "MIT",