vellum-cli 0.2.0 → 0.3.1

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 +25 -0
  2. package/dist/index.js +353 -22
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -71,6 +71,31 @@ vellum push artifact.html --page-id pg_123 --json
71
71
  On success it prints the new version number, the human view URL, and the raw
72
72
  URL. With `--json` it prints the server's `CreateVersionResponse` verbatim.
73
73
 
74
+ ### `vellum markup <page-id>`
75
+
76
+ Pin a comment to a passage of a published document. The `--quote` text must
77
+ appear verbatim in the document; the viewer highlights it and links your note to
78
+ the highlight. Without `--version`, the page's current version is used.
79
+
80
+ ```bash
81
+ # anchor a note to a passage
82
+ vellum markup pg_123 --quote "Q3 dashboard" --body "tighten this headline"
83
+
84
+ # pipe the note in on stdin instead of --body
85
+ echo "tighten this headline" | vellum markup pg_123 --quote "Q3 dashboard"
86
+ ```
87
+
88
+ The CLI checks the quote is anchorable before posting; pass `--force` to post
89
+ anyway, or `--json` for the raw response.
90
+
91
+ ### `vellum whoami`
92
+
93
+ Show the identity the CLI is authenticated as for a server.
94
+
95
+ ### `vellum logout`
96
+
97
+ Revoke this machine's stored token server-side and forget it locally.
98
+
74
99
  ## Development
75
100
 
76
101
  From the repo root, Bun runs the TypeScript source directly (no build):
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ async function clearToken(baseUrl) {
61
61
  return existing;
62
62
  }
63
63
 
64
- // src/commands/login.ts
64
+ // src/commands/archive.ts
65
65
  import { parseArgs } from "node:util";
66
66
  // ../core/src/client.ts
67
67
  class VellumApiError extends Error {
@@ -113,6 +113,51 @@ class VellumClient {
113
113
  await this.fail(res);
114
114
  return await res.json();
115
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 listPages(opts = {}) {
123
+ const url = new URL(`${this.baseUrl}/v1/pages`);
124
+ if (opts.archived)
125
+ url.searchParams.set("status", "archived");
126
+ else if (opts.all)
127
+ url.searchParams.set("include", "archived");
128
+ const res = await this.fetchImpl(url, { headers: this.authHeaders() });
129
+ if (!res.ok)
130
+ await this.fail(res);
131
+ return await res.json();
132
+ }
133
+ async archivePage(pageId) {
134
+ return this.setArchived(pageId, true);
135
+ }
136
+ async unarchivePage(pageId) {
137
+ return this.setArchived(pageId, false);
138
+ }
139
+ async setArchived(pageId, archived) {
140
+ const action = archived ? "archive" : "unarchive";
141
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/pages/${encodeURIComponent(pageId)}/${action}`, { method: "POST", headers: this.authHeaders() });
142
+ if (!res.ok)
143
+ await this.fail(res);
144
+ return await res.json();
145
+ }
146
+ async createComment(pageId, opts) {
147
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/pages/${encodeURIComponent(pageId)}/comments`, {
148
+ method: "POST",
149
+ headers: { "Content-Type": "application/json", ...this.authHeaders() },
150
+ body: JSON.stringify({
151
+ version_id: opts.versionId,
152
+ body: opts.body,
153
+ anchor: opts.anchor,
154
+ parent_id: opts.parentId
155
+ })
156
+ });
157
+ if (!res.ok)
158
+ await this.fail(res);
159
+ return await res.json();
160
+ }
116
161
  async startCliAuth() {
117
162
  const res = await this.fetchImpl(`${this.baseUrl}/v1/cli/auth/start`, {
118
163
  method: "POST",
@@ -150,6 +195,164 @@ class VellumClient {
150
195
  await this.fail(res);
151
196
  }
152
197
  }
198
+ // src/commands/archive.ts
199
+ function help(verb) {
200
+ const what = verb === "archive" ? "hide a page from the gallery and make it read-only" : "restore an archived page to live";
201
+ return `vellum ${verb} — ${what}
202
+
203
+ Usage:
204
+ vellum ${verb} <page-id> [options]
205
+
206
+ ${verb === "archive" ? `Archiving keeps the bytes, versions, comments, and URLs intact — new
207
+ versions and comments are paused until you unarchive. Owner/admin only.` : `Unarchiving restores the page to the gallery and re-opens it for new
208
+ versions and comments. Owner/admin only.`}
209
+
210
+ Options:
211
+ --url <url> Server base URL (env: VELLUM_URL)
212
+ --api-key <key> Shared API key (env: VELLUM_API_KEY)
213
+ --json Print the raw JSON response
214
+ -h, --help Show this help`;
215
+ }
216
+ async function run(verb, argv) {
217
+ const { values, positionals } = parseArgs({
218
+ args: argv,
219
+ allowPositionals: true,
220
+ options: {
221
+ url: { type: "string" },
222
+ "api-key": { type: "string" },
223
+ json: { type: "boolean", default: false },
224
+ help: { type: "boolean", short: "h", default: false }
225
+ }
226
+ });
227
+ if (values.help) {
228
+ console.log(help(verb));
229
+ return 0;
230
+ }
231
+ const pageId = positionals[0];
232
+ if (!pageId) {
233
+ console.error(`Error: missing <page-id>.
234
+ `);
235
+ console.error(help(verb));
236
+ return 1;
237
+ }
238
+ const flags = { url: values.url, apiKey: values["api-key"] };
239
+ const baseUrl = resolveBaseUrl(flags);
240
+ const credential = await resolveCredential(flags, baseUrl);
241
+ const client2 = new VellumClient({ baseUrl, ...credential });
242
+ try {
243
+ const res = verb === "archive" ? await client2.archivePage(pageId) : await client2.unarchivePage(pageId);
244
+ if (values.json) {
245
+ console.log(JSON.stringify(res, null, 2));
246
+ } else {
247
+ console.log(`✓ ${verb}d ${res.id}`);
248
+ }
249
+ return 0;
250
+ } catch (err) {
251
+ if (err instanceof VellumApiError && err.status === 401) {
252
+ console.error(`Error: ${verb} requires the owner API key. Set VELLUM_API_KEY or pass --api-key.`);
253
+ return 1;
254
+ }
255
+ if (err instanceof VellumApiError && err.status === 404) {
256
+ console.error(`Error: no page found at ${pageId}.`);
257
+ return 1;
258
+ }
259
+ throw err;
260
+ }
261
+ }
262
+ function archiveCommand(argv) {
263
+ return run("archive", argv);
264
+ }
265
+ function unarchiveCommand(argv) {
266
+ return run("unarchive", argv);
267
+ }
268
+
269
+ // src/commands/list.ts
270
+ import { parseArgs as parseArgs2 } from "node:util";
271
+ var HELP = `vellum list — list your artifacts (owner/admin only)
272
+
273
+ Usage:
274
+ vellum list [options]
275
+
276
+ By default only live pages are shown. Use --archived for archived-only, or
277
+ --all for both. Listing requires the owner API key (VELLUM_API_KEY / --api-key).
278
+
279
+ Options:
280
+ --archived Show only archived pages
281
+ --all Show both live and archived pages
282
+ --url <url> Server base URL (env: VELLUM_URL)
283
+ --api-key <key> Shared API key (env: VELLUM_API_KEY)
284
+ --json Print the raw JSON response
285
+ -h, --help Show this help`;
286
+ function cell(value, width) {
287
+ if (value.length > width)
288
+ return `${value.slice(0, width - 1)}…`;
289
+ return value.padEnd(width);
290
+ }
291
+ function renderTable(pages) {
292
+ const lines = [
293
+ `${cell("PAGE", 26)} ${cell("LATEST", 7)} ${cell("VERS", 5)} ${cell("COMM", 5)} TITLE`
294
+ ];
295
+ for (const p of pages) {
296
+ const id = p.archived_at ? `${p.id} *` : p.id;
297
+ const latest = p.latest_version != null ? `v${p.latest_version}` : "—";
298
+ lines.push(`${cell(id, 26)} ${cell(latest, 7)} ${cell(String(p.version_count), 5)} ` + `${cell(String(p.comment_count), 5)} ${p.title ?? ""}`.trimEnd());
299
+ }
300
+ return lines.join(`
301
+ `);
302
+ }
303
+ async function listCommand(argv) {
304
+ const { values } = parseArgs2({
305
+ args: argv,
306
+ allowPositionals: false,
307
+ options: {
308
+ archived: { type: "boolean", default: false },
309
+ all: { type: "boolean", default: false },
310
+ url: { type: "string" },
311
+ "api-key": { type: "string" },
312
+ json: { type: "boolean", default: false },
313
+ help: { type: "boolean", short: "h", default: false }
314
+ }
315
+ });
316
+ if (values.help) {
317
+ console.log(HELP);
318
+ return 0;
319
+ }
320
+ const flags = { url: values.url, apiKey: values["api-key"] };
321
+ const baseUrl = resolveBaseUrl(flags);
322
+ const credential = await resolveCredential(flags, baseUrl);
323
+ const client2 = new VellumClient({ baseUrl, ...credential });
324
+ try {
325
+ const { pages } = await client2.listPages({
326
+ archived: values.archived,
327
+ all: values.all
328
+ });
329
+ if (values.json) {
330
+ console.log(JSON.stringify(pages, null, 2));
331
+ return 0;
332
+ }
333
+ if (pages.length === 0) {
334
+ const scope = values.archived ? "archived " : "";
335
+ console.log(`No ${scope}pages.`);
336
+ return 0;
337
+ }
338
+ console.log(renderTable(pages));
339
+ const archivedCount = pages.filter((p) => p.archived_at).length;
340
+ const suffix = archivedCount ? ` (${archivedCount} archived, marked *)` : "";
341
+ console.log(`
342
+ ${pages.length} page${pages.length === 1 ? "" : "s"}${suffix}`);
343
+ return 0;
344
+ } catch (err) {
345
+ if (err instanceof VellumApiError && err.status === 401) {
346
+ console.error("Error: list requires the owner API key. Set VELLUM_API_KEY or pass --api-key.");
347
+ return 1;
348
+ }
349
+ throw err;
350
+ }
351
+ }
352
+
353
+ // src/commands/login.ts
354
+ import { parseArgs as parseArgs3 } from "node:util";
355
+
153
356
  // src/util/open.ts
154
357
  import { spawn } from "node:child_process";
155
358
  function openBrowser(url) {
@@ -166,7 +369,7 @@ function openBrowser(url) {
166
369
  }
167
370
 
168
371
  // src/commands/login.ts
169
- var HELP = `vellum login — authenticate this machine to a Vellum server
372
+ var HELP2 = `vellum login — authenticate this machine to a Vellum server
170
373
 
171
374
  Opens your browser to approve a CLI login, then stores a token under
172
375
  ~/.config/vellum/config.json so future commands authenticate as you.
@@ -180,7 +383,7 @@ Options:
180
383
  -h, --help Show this help`;
181
384
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
182
385
  async function loginCommand(argv) {
183
- const { values } = parseArgs({
386
+ const { values } = parseArgs3({
184
387
  args: argv,
185
388
  options: {
186
389
  url: { type: "string" },
@@ -189,7 +392,7 @@ async function loginCommand(argv) {
189
392
  }
190
393
  });
191
394
  if (values.help) {
192
- console.log(HELP);
395
+ console.log(HELP2);
193
396
  return 0;
194
397
  }
195
398
  const baseUrl = resolveBaseUrl({ url: values.url });
@@ -229,8 +432,8 @@ To authorize this CLI, visit:
229
432
  }
230
433
 
231
434
  // src/commands/logout.ts
232
- import { parseArgs as parseArgs2 } from "node:util";
233
- var HELP2 = `vellum logout — remove this machine's stored CLI token
435
+ import { parseArgs as parseArgs4 } from "node:util";
436
+ var HELP3 = `vellum logout — remove this machine's stored CLI token
234
437
 
235
438
  Usage:
236
439
  vellum logout [options]
@@ -239,7 +442,7 @@ Options:
239
442
  --url <url> Server base URL (env: VELLUM_URL)
240
443
  -h, --help Show this help`;
241
444
  async function logoutCommand(argv) {
242
- const { values } = parseArgs2({
445
+ const { values } = parseArgs4({
243
446
  args: argv,
244
447
  options: {
245
448
  url: { type: "string" },
@@ -247,7 +450,7 @@ async function logoutCommand(argv) {
247
450
  }
248
451
  });
249
452
  if (values.help) {
250
- console.log(HELP2);
453
+ console.log(HELP3);
251
454
  return 0;
252
455
  }
253
456
  const baseUrl = resolveBaseUrl({ url: values.url });
@@ -263,10 +466,130 @@ async function logoutCommand(argv) {
263
466
  return 0;
264
467
  }
265
468
 
469
+ // src/commands/markup.ts
470
+ import { parseArgs as parseArgs5 } from "node:util";
471
+ var HELP4 = `vellum markup — pin a comment to a passage of a document
472
+
473
+ Usage:
474
+ vellum markup <page-id> --quote "<text>" --body "<note>" [options]
475
+ echo "<note>" | vellum markup <page-id> --quote "<text>"
476
+
477
+ The --quote text must appear verbatim in the document; the viewer highlights it
478
+ and links the highlight to your note. Without --version, the page's current
479
+ version is used.
480
+
481
+ Options:
482
+ --quote <text> Passage to mark up (must match the document text)
483
+ --body <note> Your note (or pipe it via stdin)
484
+ --version <n> Pin to a specific version number (default: current)
485
+ --force Post even if the quote isn't found in the version
486
+ --url <url> Server base URL (env: VELLUM_URL)
487
+ --api-key <key> Shared API key (env: VELLUM_API_KEY)
488
+ --json Print the raw JSON response
489
+ -h, --help Show this help`;
490
+ async function readStdin() {
491
+ const chunks = [];
492
+ for await (const chunk of process.stdin)
493
+ chunks.push(chunk);
494
+ return Buffer.concat(chunks).toString("utf8");
495
+ }
496
+ function resolveVersion(page, requested) {
497
+ if (requested != null) {
498
+ return page.versions.find((v) => v.version_number === requested) ?? null;
499
+ }
500
+ const current = page.current_version_id ? page.versions.find((v) => v.id === page.current_version_id) : undefined;
501
+ return current ?? page.versions[page.versions.length - 1] ?? null;
502
+ }
503
+ function extractText(html) {
504
+ 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, "'");
505
+ }
506
+ var collapse = (s) => s.replace(/\s+/g, " ").trim();
507
+ function quoteIsAnchorable(html, quote) {
508
+ const text = extractText(html);
509
+ return text.includes(quote) || collapse(text).includes(collapse(quote));
510
+ }
511
+ async function markupCommand(argv) {
512
+ const { values, positionals } = parseArgs5({
513
+ args: argv,
514
+ allowPositionals: true,
515
+ options: {
516
+ quote: { type: "string" },
517
+ body: { type: "string" },
518
+ version: { type: "string" },
519
+ force: { type: "boolean", default: false },
520
+ url: { type: "string" },
521
+ "api-key": { type: "string" },
522
+ json: { type: "boolean", default: false },
523
+ help: { type: "boolean", short: "h", default: false }
524
+ }
525
+ });
526
+ if (values.help) {
527
+ console.log(HELP4);
528
+ return 0;
529
+ }
530
+ const pageId = positionals[0];
531
+ if (!pageId) {
532
+ console.error(`Error: missing <page-id>.
533
+ `);
534
+ console.error(HELP4);
535
+ return 1;
536
+ }
537
+ const quote = (values.quote ?? "").trim();
538
+ if (!quote) {
539
+ console.error("Error: --quote is required (the passage to mark up).");
540
+ return 1;
541
+ }
542
+ const body = (values.body ?? await readStdin()).trim();
543
+ if (!body) {
544
+ console.error("Error: no note provided (pass --body or pipe it via stdin).");
545
+ return 1;
546
+ }
547
+ let version;
548
+ if (values.version != null) {
549
+ version = Number.parseInt(values.version, 10);
550
+ if (!Number.isInteger(version) || version < 1) {
551
+ console.error("Error: --version must be a positive integer.");
552
+ return 1;
553
+ }
554
+ }
555
+ const flags = { url: values.url, apiKey: values["api-key"] };
556
+ const baseUrl = resolveBaseUrl(flags);
557
+ const credential = await resolveCredential(flags, baseUrl);
558
+ const client2 = new VellumClient({ baseUrl, ...credential });
559
+ const page = await client2.getPage(pageId);
560
+ const target = resolveVersion(page, version);
561
+ if (!target) {
562
+ console.error(version != null ? `Error: page has no version ${version}.` : "Error: page has no versions to mark up.");
563
+ return 1;
564
+ }
565
+ if (!values.force) {
566
+ const html = await fetch(target.raw_url).then((r) => r.ok ? r.text() : "");
567
+ if (!quoteIsAnchorable(html, quote)) {
568
+ console.error(`Error: --quote was not found in v${target.version_number}; the highlight
569
+ ` + " would not appear. Check the wording, or pass --force to post anyway.");
570
+ return 1;
571
+ }
572
+ }
573
+ const anchor = { type: "text-quote", exact: quote };
574
+ const comment = await client2.createComment(pageId, {
575
+ versionId: target.id,
576
+ body,
577
+ anchor
578
+ });
579
+ if (values.json) {
580
+ console.log(JSON.stringify(comment, null, 2));
581
+ } else {
582
+ console.log("✓ markup added");
583
+ console.log(` on: “${quote.length > 80 ? `${quote.slice(0, 79)}…` : quote}”`);
584
+ console.log(` view: ${page.view_url}`);
585
+ }
586
+ return 0;
587
+ }
588
+
266
589
  // src/commands/push.ts
267
590
  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
591
+ import { parseArgs as parseArgs6 } from "node:util";
592
+ var HELP5 = `vellum push — create a Vellum artifact from HTML
270
593
 
271
594
  Usage:
272
595
  vellum push <file.html> [options]
@@ -278,14 +601,14 @@ Options:
278
601
  --api-key <key> Shared API key (env: VELLUM_API_KEY)
279
602
  --json Print the raw JSON response
280
603
  -h, --help Show this help`;
281
- async function readStdin() {
604
+ async function readStdin2() {
282
605
  const chunks = [];
283
606
  for await (const chunk of process.stdin)
284
607
  chunks.push(chunk);
285
608
  return Buffer.concat(chunks).toString("utf8");
286
609
  }
287
610
  async function pushCommand(argv) {
288
- const { values, positionals } = parseArgs3({
611
+ const { values, positionals } = parseArgs6({
289
612
  args: argv,
290
613
  allowPositionals: true,
291
614
  options: {
@@ -297,11 +620,11 @@ async function pushCommand(argv) {
297
620
  }
298
621
  });
299
622
  if (values.help) {
300
- console.log(HELP3);
623
+ console.log(HELP5);
301
624
  return 0;
302
625
  }
303
626
  const file = positionals[0];
304
- const html = file ? await readFile2(file, "utf8") : await readStdin();
627
+ const html = file ? await readFile2(file, "utf8") : await readStdin2();
305
628
  if (!html.trim()) {
306
629
  console.error("Error: no HTML provided (empty file or stdin).");
307
630
  return 1;
@@ -322,8 +645,8 @@ async function pushCommand(argv) {
322
645
  }
323
646
 
324
647
  // 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
648
+ import { parseArgs as parseArgs7 } from "node:util";
649
+ var HELP6 = `vellum whoami — show the identity the CLI is authenticated as
327
650
 
328
651
  Usage:
329
652
  vellum whoami [options]
@@ -333,7 +656,7 @@ Options:
333
656
  --api-key <key> Shared API key (env: VELLUM_API_KEY)
334
657
  -h, --help Show this help`;
335
658
  async function whoamiCommand(argv) {
336
- const { values } = parseArgs4({
659
+ const { values } = parseArgs7({
337
660
  args: argv,
338
661
  options: {
339
662
  url: { type: "string" },
@@ -342,7 +665,7 @@ async function whoamiCommand(argv) {
342
665
  }
343
666
  });
344
667
  if (values.help) {
345
- console.log(HELP4);
668
+ console.log(HELP6);
346
669
  return 0;
347
670
  }
348
671
  const baseUrl = resolveBaseUrl({ url: values.url });
@@ -358,7 +681,7 @@ async function whoamiCommand(argv) {
358
681
  }
359
682
 
360
683
  // src/index.ts
361
- var HELP5 = `vellum — CLI for the Vellum artifact store
684
+ var HELP7 = `vellum — CLI for the Vellum artifact store
362
685
 
363
686
  Usage:
364
687
  vellum <command> [options]
@@ -368,25 +691,33 @@ Commands:
368
691
  logout Remove this machine's stored CLI token
369
692
  whoami Show the identity the CLI is authenticated as
370
693
  push Create an artifact (page) from HTML (file or stdin)
694
+ list List your artifacts (owner/admin only)
695
+ markup Pin a comment to a passage of a document
696
+ archive Archive a page (read-only, hidden from the gallery)
697
+ unarchive Restore an archived page to live
371
698
 
372
699
  Run 'vellum <command> --help' for command-specific options.`;
373
700
  var commands = {
374
701
  login: loginCommand,
375
702
  logout: logoutCommand,
376
703
  whoami: whoamiCommand,
377
- push: pushCommand
704
+ push: pushCommand,
705
+ list: listCommand,
706
+ markup: markupCommand,
707
+ archive: archiveCommand,
708
+ unarchive: unarchiveCommand
378
709
  };
379
710
  async function main() {
380
711
  const [, , cmd, ...rest] = process.argv;
381
712
  if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") {
382
- console.log(HELP5);
713
+ console.log(HELP7);
383
714
  return cmd ? 0 : 1;
384
715
  }
385
716
  const handler = commands[cmd];
386
717
  if (!handler) {
387
718
  console.error(`Unknown command: ${cmd}
388
719
  `);
389
- console.error(HELP5);
720
+ console.error(HELP7);
390
721
  return 1;
391
722
  }
392
723
  return handler(rest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vellum-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "The vellum CLI — publish agent-authored HTML artifacts to a Vellum server.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,9 +28,10 @@
28
28
  "start": "bun run src/index.ts",
29
29
  "build": "bun build src/index.ts --target=node --format=esm --outfile=dist/index.js",
30
30
  "prepack": "bun run build",
31
+ "release": "bun publish --access public",
31
32
  "typecheck": "tsc -p tsconfig.json --noEmit"
32
33
  },
33
34
  "devDependencies": {
34
- "@vellum/core": "workspace:*"
35
+ "@vellum/core": "0.1.0"
35
36
  }
36
37
  }